<?php
/**
 * Notifier registry + dispatcher.
 *
 * Configured notifiers are stored in the `backreload_notifiers` option as:
 *   [
 *     [ 'id' => 'n_xxx', 'type' => 'slack', 'name' => 'Slack', 'enabled' => true,
 *       'on_success' => true, 'on_failure' => true, 'config' => [...] ],
 *     ...
 *   ]
 *
 * @package Backreload\Notifiers
 */

namespace Backreload\Notifiers;

use Backreload\Plugin;
use Backreload\Models\BackupRecord;

if ( ! defined( 'ABSPATH' ) ) { exit; }

class NotifierManager {

	const OPTION = 'backreload_notifiers';

	/** @var array<string,class-string<Notifier>> */
	private array $types = [];

	private Plugin $plugin;

	public function __construct( Plugin $plugin ) {
		$this->plugin = $plugin;
	}

	public function boot(): void {
		$built_in = [
			'email'    => EmailNotifier::class,
			'telegram' => TelegramNotifier::class,
			'slack'    => SlackNotifier::class,
			'discord'  => DiscordNotifier::class,
			'pushover' => PushoverNotifier::class,
			'teams'    => TeamsNotifier::class,
			'gchat'    => GoogleChatNotifier::class,
			'webhook'  => WebhookNotifier::class,
		];
		$this->types = apply_filters( 'backreload_register_notifiers', $built_in );

		add_action( 'backreload_completed',          [ $this, 'dispatch_success' ],             10, 1 );
		add_action( 'backreload_failed',             [ $this, 'dispatch_failure' ],             10, 2 );
		add_action( 'backreload_destination_failed', [ $this, 'dispatch_destination_failure' ], 10, 4 );
	}

	/** @return array<string,class-string<Notifier>> */
	public function types(): array { return $this->types; }

	public function has( string $type ): bool { return isset( $this->types[ $type ] ); }

	public function handler( string $type ): Notifier {
		if ( ! $this->has( $type ) ) {
			throw new \InvalidArgumentException( esc_html( "Unknown notifier type: {$type}" ) );
		}
		$cls = $this->types[ $type ];
		return new $cls();
	}

	public function catalog(): array {
		$out = [];
		foreach ( $this->types as $type => $cls ) {
			$out[] = [
				'type'        => $type,
				'label'       => $cls::label(),
				'description' => $cls::description(),
				'schema'      => $cls::config_schema(),
			];
		}
		return $out;
	}

	/** @return array<int,array<string,mixed>> */
	public function configured(): array {
		$items = (array) get_option( self::OPTION, [] );
		$enc = null;
		try { $enc = $this->plugin->get( 'encryption' ); } catch ( \Throwable $e ) {}

		if ( $enc ) {
			foreach ( $items as &$it ) {
				if ( isset( $it['config'] ) && is_array( $it['config'] ) ) {
					$it['config'] = $enc->decrypt_config( $it['config'] );
				}
			}
		}
		return $items;
	}

	public function save_configured( array $items ): void {
		$enc = null;
		try { $enc = $this->plugin->get( 'encryption' ); } catch ( \Throwable $e ) {}

		if ( $enc ) {
			foreach ( $items as &$it ) {
				if ( isset( $it['config'] ) && is_array( $it['config'] ) ) {
					$it['config'] = $enc->encrypt_config( $it['config'] );
				}
			}
		}
		update_option( self::OPTION, array_values( $items ) );
	}

	public function dispatch_success( BackupRecord $rec ): void {
		$this->dispatch( $rec, true );
	}

	public function dispatch_failure( BackupRecord $rec, \Throwable $err ): void {
		$this->dispatch( $rec, false, $err );
	}

	/**
	 * Fire when a single destination upload fails (after all retries). Reuses
	 * the existing `on_failure` toggle so users who opted in to failure alerts
	 * also hear about a remote going down — the rest of the backup may have
	 * succeeded elsewhere, but a broken destination is something they need to
	 * know about. Throttled to once per (backup, destination) in the engine.
	 *
	 * @param BackupRecord      $rec
	 * @param DestinationRecord $dest
	 * @param array             $file  files[] entry whose upload failed
	 * @param \Throwable        $err
	 */
	public function dispatch_destination_failure( BackupRecord $rec, \Backreload\Models\DestinationRecord $dest, array $file, \Throwable $err ): void {
		$payload = $this->build_destination_failure_payload( $rec, $dest, $file, $err );
		$this->fan_out( $payload, 'on_failure', $rec );
	}

	/**
	 * Fan a built payload out to every enabled notifier that opts in to the
	 * given flag (`on_success` or `on_failure`). All channels see every event
	 * they're subscribed to — failure on one notifier doesn't suppress the
	 * others.
	 */
	private function fan_out( array $payload, string $flag, BackupRecord $rec ): void {
		$logger = $this->plugin->get( 'logger' );
		foreach ( $this->configured() as $item ) {
			if ( empty( $item['enabled'] ) ) continue;
			if ( empty( $item[ $flag ] ) ) continue;
			if ( ! $this->has( $item['type'] ) ) continue;

			try {
				$this->handler( $item['type'] )->send( $item['config'] ?? [], $payload );
				$logger->info( "Notification sent via {$item['type']}", [], $rec->ext_id );
			} catch ( \Throwable $e ) {
				// Notifier errors are NEVER fatal — one broken Slack URL must
				// not silently stop the next configured channel.
				$logger->warn( "Notification failed ({$item['type']}): " . $e->getMessage(), [], $rec->ext_id );
			}
		}
	}

	private function dispatch( BackupRecord $rec, bool $success, ?\Throwable $err = null ): void {
		$payload = $this->build_payload( $rec, $success, $err );
		$this->fan_out( $payload, $success ? 'on_success' : 'on_failure', $rec );
	}

	private function build_payload( BackupRecord $rec, bool $success, ?\Throwable $err ): array {
		$site = wp_parse_url( home_url(), PHP_URL_HOST );
		return [
			'title'   => $success
				? "Backup completed on {$site}"
				: "Backup failed on {$site}",
			'message' => $success
				? sprintf( "%s · %s · %ds", $rec->type, size_format( $rec->size ), $rec->duration )
				: ( $err ? $err->getMessage() : 'Unknown error' ),
			'level'   => $success ? 'success' : 'error',
			'context' => [
				'backup_id' => $rec->ext_id,
				'type'      => $rec->type,
				'size'      => $rec->size,
				'duration'  => $rec->duration,
				'site_url'  => home_url(),
			],
		];
	}

	private function build_destination_failure_payload( BackupRecord $rec, \Backreload\Models\DestinationRecord $dest, array $file, \Throwable $err ): array {
		$site = wp_parse_url( home_url(), PHP_URL_HOST );
		$part = (string) ( $file['name'] ?? '' );
		return [
			'title'   => "Destination \"{$dest->name}\" failing on {$site}",
			'message' => sprintf(
				"Upload to %s (%s) failed%s: %s",
				$dest->name,
				$dest->type,
				$part !== '' ? " for {$part}" : '',
				$err->getMessage()
			),
			'level'   => 'error',
			'context' => [
				'event'            => 'destination_failed',
				'backup_id'        => $rec->ext_id,
				'destination_id'   => $dest->ext_id,
				'destination_name' => $dest->name,
				'destination_type' => $dest->type,
				'file'             => $part,
				'site_url'         => home_url(),
			],
		];
	}
}
