<?php
/**
 * @package Hafenstudios\Wellenbrecher
 */

namespace Hafenstudios\Wellenbrecher;

use Hafenstudios\Core\Core;
use Hafenstudios\Core\Settings\Settings;
use Hafenstudios\Wellenbrecher\Admin\CommentsColumn;
use Hafenstudios\Wellenbrecher\Admin\ExternaPage;
use Hafenstudios\Wellenbrecher\Admin\LogPage;
use Hafenstudios\Wellenbrecher\Admin\Menu;
use Hafenstudios\Wellenbrecher\Admin\OverviewPage;
use Hafenstudios\Wellenbrecher\Admin\QuarantinePage;
use Hafenstudios\Wellenbrecher\Admin\ReportsExport;
use Hafenstudios\Wellenbrecher\Admin\SettingsPage;
use Hafenstudios\Wellenbrecher\Channels\CommentChannel;
use Hafenstudios\Wellenbrecher\Channels\CommentFormFields;
use Hafenstudios\Wellenbrecher\Channels\LeadlotseChannel;
use Hafenstudios\Wellenbrecher\Channels\LeadlotseFormFields;
use Hafenstudios\Wellenbrecher\Channels\RegistrationChannel;
use Hafenstudios\Wellenbrecher\Channels\WooCheckoutChannel;
use Hafenstudios\Wellenbrecher\Db\CommentLookup;
use Hafenstudios\Wellenbrecher\Db\SpamLogRepository;
use Hafenstudios\Wellenbrecher\Db\StatsRepository;
use Hafenstudios\Wellenbrecher\Db\TokenRepository;
use Hafenstudios\Wellenbrecher\Engine\ScoreEngine;
use Hafenstudios\Wellenbrecher\Install\Installer;
use Hafenstudios\Wellenbrecher\Privacy\PrivacyBridge;
use Hafenstudios\Wellenbrecher\Pro\Bayes\Tokenizer;
use Hafenstudios\Wellenbrecher\Pro\Bayes\Trainer;
use Hafenstudios\Wellenbrecher\Pro\Disposable\DisposableUpdater;
use Hafenstudios\Wellenbrecher\Pro\GeoIp\GeoDownloader;
use Hafenstudios\Wellenbrecher\Rest\TokenController;
use Hafenstudios\Wellenbrecher\Rules\RuleRegistry;
use Hafenstudios\Wellenbrecher\Signals\FieldRotation;
use Hafenstudios\Wellenbrecher\Signals\PowChallenge;
use Hafenstudios\Wellenbrecher\Signals\TokenService;
use Hafenstudios\Wellenbrecher\Support\Allowlist;
use Hafenstudios\Wellenbrecher\Support\IpHasher;
use Hafenstudios\Wellenbrecher\Support\ObservationMode;
use Hafenstudios\Wellenbrecher\Support\ProGate;
use Hafenstudios\Wellenbrecher\Support\RequestSnapshot;
use Hafenstudios\Wellenbrecher\Support\SelfUpdater;

defined( 'ABSPATH' ) || exit;

/**
 * Boot und Verdrahtung des Plugins.
 *
 * Seit Etappe W2 mit Wirkung nach aussen: Kommentar-Kanal (preprocess/
 * pre_comment_approved/Meta), Formular-Signale (Feld-Tausch, Honeypot,
 * signals.js), Token-Endpoint, Admin v1 (Einstellungen + Block-Log) und
 * die Wellenbrecher-Spalte im Kommentar-Screen. Registrierungen und die
 * Flotten-Kanaele folgen in W3.
 */
class Plugin {

	private static ?Plugin $instance = null;

	private ?Settings $settings = null;

	private ?ScoreEngine $engine = null;

	/**
	 * Liefert die Singleton-Instanz.
	 *
	 * @return Plugin
	 */
	public static function instance(): Plugin {
		return self::$instance ??= new self();
	}

	/**
	 * Bootet das Plugin (Aufruf auf plugins_loaded Prio 20, nach Core-Guard).
	 *
	 * @return void
	 */
	public function boot(): void {
		Core::instance()->register_plugin(
			'wellenbrecher',
			array(
				'name'      => 'Wellenbrecher',
				'version'   => defined( 'WELLENBRECHER_VERSION' ) ? WELLENBRECHER_VERSION : '1.0.0',
				'menu_slug' => 'wellenbrecher',
			)
		);

		add_action( 'init', array( $this, 'on_init' ) );
		add_action( Installer::CRON_HOOK, array( $this, 'run_daily_purge' ) );

		$settings    = $this->settings();
		$hasher      = new IpHasher();
		$log         = new SpamLogRepository();
		$stats       = new StatsRepository();
		$observation = new ObservationMode( $settings );

		// Pro-Gate (WB19): steuert alle W5-Funktionen. Mit inaktivem Gate bleibt
		// das Plugin exakt wie 1.0.0.
		$pro         = ProGate::enabled();
		$pow_enabled = $pro && ! empty( $settings->get( 'pro.pow.enabled', false ) );

		// Kommentar-Kanal (W2). Die Engine kommt lazy per Closure, damit die
		// Regel-Listen (Disposable-Set usw.) nicht auf jedem Request laden.
		$channel = new CommentChannel(
			fn(): ScoreEngine => $this->engine(),
			$settings,
			$log,
			$stats,
			$hasher,
			new FieldRotation(),
			new Allowlist( $settings ),
			$observation,
			new CommentLookup(),
			new RequestSnapshot( $hasher )
		);
		$channel->register();

		// Frontend-Signale im Kommentarformular (inkl. PoW-Feld bei Pro + Setting).
		( new CommentFormFields( new FieldRotation(), $pow_enabled ) )->register();

		// Registrierungs-Kanal (W3): WP-Single-Site, Multisite, WooCommerce.
		( new RegistrationChannel(
			fn(): ScoreEngine => $this->engine(),
			$settings,
			$log,
			$stats,
			$hasher,
			new Allowlist( $settings ),
			$observation,
			new CommentLookup(),
			new RequestSnapshot( $hasher )
		) )->register();

		// Leadlotse-Kanal (W3): lauscht auf den Leadlotse-Spam-Filter und
		// injiziert die Signal-Hidden-Felder + signals.js in Leadlotse-Formulare.
		( new LeadlotseChannel(
			fn(): ScoreEngine => $this->engine(),
			$settings,
			$log,
			$stats,
			$hasher,
			new Allowlist( $settings ),
			$observation,
			new CommentLookup(),
			new RequestSnapshot( $hasher )
		) )->register();
		( new LeadlotseFormFields( $pow_enabled ) )->register();

		// DSGVO: Datenschutz-Snippet, Export und Loeschung (Bauplan Kap. 9).
		( new PrivacyBridge( $log ) )->register();

		// Pro-Paket (W5a): registriert sich nur bei aktivem Gate. Bayes-Training
		// aus Moderations-Feedback, Woo-Checkout (mit WooCommerce) und Reports.
		if ( $pro ) {
			( new Trainer( new TokenRepository(), new Tokenizer(), $settings ) )->register();

			if ( class_exists( 'WooCommerce' ) || function_exists( 'WC' ) ) {
				( new WooCheckoutChannel(
					fn(): ScoreEngine => $this->engine(),
					$settings,
					$log,
					$stats,
					$hasher,
					new Allowlist( $settings ),
					$observation,
					new CommentLookup(),
					new RequestSnapshot( $hasher ),
					$pow_enabled
				) )->register();
			}

			( new ReportsExport( $stats ) )->register();

			// W5b Opt-in-Externa: eigene Monats-Cron-Wiederholung anmelden und die
			// Refresh-Hooks verdrahten. Die Events werden erst bei Feature-Opt-in
			// geplant (ExternaPage::handle_save); hier haengen nur die Handler.
			add_filter(
				'cron_schedules',
				static function ( $schedules ) {
					if ( is_array( $schedules ) && ! isset( $schedules[ GeoDownloader::CRON_SCHEDULE ] ) ) {
						$schedules[ GeoDownloader::CRON_SCHEDULE ] = array(
							'interval' => 30 * DAY_IN_SECONDS,
							'display'  => __( 'Einmal im Monat (Wellenbrecher)', 'wellenbrecher' ),
						);
					}
					return $schedules;
				}
			);
			add_action(
				GeoDownloader::CRON_HOOK,
				static function () use ( $settings ): void {
					( new GeoDownloader( $settings ) )->refresh();
				}
			);
			add_action(
				DisposableUpdater::CRON_HOOK,
				static function () use ( $settings ): void {
					( new DisposableUpdater( $settings ) )->refresh();
				}
			);
		}

		// Selbst-Update gegen den statischen Endpoint auf hafenstudios.com (WB12).
		// Host-gebunden + fail-silent; der WP-Kern feuert den Filter anhand des
		// "Update URI"-Headers.
		( new SelfUpdater(
			plugin_basename( WELLENBRECHER_FILE ),
			defined( 'WELLENBRECHER_VERSION' ) ? WELLENBRECHER_VERSION : '1.0.0'
		) )->register();

		// Oeffentlicher Token-Endpoint (liefert bei Pro + PoW-Setting zusaetzlich
		// eine eskalierende Challenge fuer vorbelastete IPs).
		add_action(
			'rest_api_init',
			function () use ( $pow_enabled ): void {
				$pow_hasher    = null;
				$pow_log       = null;
				$pow_challenge = null;
				$pow_difficulty = PowChallenge::DEFAULT_DIFFICULTY;
				if ( $pow_enabled ) {
					$pow_hasher     = new IpHasher();
					$pow_log        = new SpamLogRepository();
					$pow_challenge  = new PowChallenge();
					$pow_difficulty = (int) $this->settings()->get( 'pro.pow.difficulty', 4 );
				}
				( new TokenController(
					new TokenService(),
					new RequestSnapshot( new IpHasher() ),
					$this->settings(),
					$pow_hasher,
					$pow_log,
					$pow_challenge,
					$pow_difficulty
				) )->register_routes();
			}
		);

		if ( is_admin() ) {
			// Schema-Nachzug bei Plugin-Updates ohne erneute Aktivierung.
			Installer::migrate();

			$overview_page   = new OverviewPage( $settings, $stats, $log, $observation, $pro );
			$quarantine_page = new QuarantinePage( $log );
			$log_page        = new LogPage( $log );
			$settings_page   = new SettingsPage( $settings, $stats, $observation, $pro );
			$quarantine_page->register();
			$log_page->register();
			$settings_page->register();

			// Externe Dienste (Opt-in-Externa, W5b) nur bei aktivem Pro-Gate.
			$externa_page = null;
			if ( $pro ) {
				$externa_page = new ExternaPage( $settings );
				$externa_page->register();
			}

			( new Menu( $overview_page, $quarantine_page, $log_page, $settings_page, $externa_page ) )->register();
			( new CommentsColumn() )->register();
		}
	}

	/**
	 * init-Hook: Uebersetzungen laden.
	 *
	 * @return void
	 */
	public function on_init(): void {
		load_plugin_textdomain(
			'wellenbrecher',
			false,
			dirname( plugin_basename( WELLENBRECHER_FILE ) ) . '/languages'
		);
	}

	/**
	 * Settings-Namespace wellenbrecher ueber den Core.
	 *
	 * @return Settings
	 */
	public function settings(): Settings {
		return $this->settings ??= Core::instance()->settings( 'wellenbrecher', Installer::default_settings() );
	}

	/**
	 * Baut die Score-Engine mit dem Default-Regelsatz aus den Settings.
	 * Kanal-Adapter (W2/W3) holen sich die Engine hier.
	 *
	 * @return ScoreEngine
	 */
	public function engine(): ScoreEngine {
		if ( null === $this->engine ) {
			$settings = $this->settings()->all();
			$registry = new RuleRegistry(
				$settings,
				new TokenService(),
				new IpHasher(),
				new SpamLogRepository(),
				null,
				ProGate::enabled()
			);
			$this->engine = new ScoreEngine( $registry->build(), $settings );
		}
		return $this->engine;
	}

	/**
	 * Taeglicher Purge-Cron: Log-Eintraege nach DSGVO-Fristen loeschen.
	 *
	 * @return void
	 */
	public function run_daily_purge(): void {
		$repo = new SpamLogRepository();
		$repo->purge(
			(int) $this->settings()->get( 'retention.block_log_days', 7 ),
			(int) $this->settings()->get( 'retention.quarantine_days', 30 )
		);
	}

	/**
	 * Stats-Repository (stabile API fuer Kompass-Kacheln, Bauplan Kap. 10).
	 *
	 * @return StatsRepository
	 */
	public function stats(): StatsRepository {
		return new StatsRepository();
	}
}
