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

namespace Hafenstudios\Wellenbrecher\Rules;

use Hafenstudios\Core\Core;
use Hafenstudios\Core\Support\Secrets;
use Hafenstudios\Wellenbrecher\Data\Lists;
use Hafenstudios\Wellenbrecher\Db\CommentLookup;
use Hafenstudios\Wellenbrecher\Db\SpamLogRepository;
use Hafenstudios\Wellenbrecher\Db\TokenRepository;
use Hafenstudios\Wellenbrecher\Engine\RuleInterface;
use Hafenstudios\Wellenbrecher\Pro\AbuseIpdb\AbuseIpdbClient;
use Hafenstudios\Wellenbrecher\Pro\Bayes\Classifier;
use Hafenstudios\Wellenbrecher\Pro\Bayes\Tokenizer;
use Hafenstudios\Wellenbrecher\Pro\GeoIp\GeoDatabase;
use Hafenstudios\Wellenbrecher\Pro\GeoIp\GeoDownloader;
use Hafenstudios\Wellenbrecher\Signals\PowChallenge;
use Hafenstudios\Wellenbrecher\Signals\TokenService;
use Hafenstudios\Wellenbrecher\Support\IpHasher;
use Hafenstudios\Wellenbrecher\Support\Uploads;

defined( 'ABSPATH' ) || exit;

/**
 * Baut den Default-Regelsatz aus den Settings: jede Regel ist einzeln
 * schaltbar (rules.{id}.enabled) und gewichtbar (rules.{id}.points,
 * Defaults aus dem Katalog Kap. 6). Dritt-Regeln kommen NICHT hierher,
 * sondern ueber den Filter `hafenstudios/wellenbrecher_rules` der Engine.
 */
class RuleRegistry {

	/**
	 * @var array<string, mixed>
	 */
	protected array $settings;

	protected TokenService $tokens;

	protected IpHasher $hasher;

	protected ?SpamLogRepository $log;

	protected CommentLookup $comments;

	/**
	 * Pro-Regeln (Bayes, Proof-of-Work) nur bei aktivem Pro-Gate anhaengen.
	 */
	protected bool $pro;

	/**
	 * @param array<string, mixed>   $settings Settings-Baum (rules.*, lists.*, pro.*).
	 * @param TokenService           $tokens   Token-Dienst.
	 * @param IpHasher               $hasher   IP-HMAC-Helfer.
	 * @param SpamLogRepository|null $log      Spam-Log (null = Wiederholungstaeter-Regel entfaellt).
	 * @param CommentLookup|null     $comments Kommentar-Abfragen (Default: eigene Instanz).
	 * @param bool                   $pro      Pro-Gate aktiv (haengt Bayes/PoW an).
	 */
	public function __construct( array $settings, TokenService $tokens, IpHasher $hasher, ?SpamLogRepository $log = null, ?CommentLookup $comments = null, bool $pro = false ) {
		$this->settings = $settings;
		$this->tokens   = $tokens;
		$this->hasher   = $hasher;
		$this->log      = $log;
		$this->comments = $comments ?? new CommentLookup();
		$this->pro      = $pro;
	}

	/**
	 * Default-Einstellungen aller Katalog-Regeln (Punkte exakt aus der
	 * Tabelle in Bauplan Kap. 6).
	 *
	 * @return array<string, array<string, mixed>>
	 */
	public static function default_rule_settings(): array {
		return array(
			TrustApprovedEmailRule::ID => array(
				'enabled' => true,
				'points'  => TrustApprovedEmailRule::DEFAULT_POINTS,
			),
			TrustUserHistoryRule::ID   => array(
				'enabled' => true,
				'points'  => TrustUserHistoryRule::DEFAULT_POINTS,
			),
			TrustVerifiedBuyerRule::ID => array(
				'enabled' => true,
				'points'  => TrustVerifiedBuyerRule::DEFAULT_POINTS,
			),
			TrustAllowlistRule::ID     => array(
				'enabled' => true,
				'points'  => TrustAllowlistRule::DEFAULT_POINTS,
			),
			HoneypotRule::ID           => array(
				'enabled' => true,
				'points'  => HoneypotRule::DEFAULT_POINTS,
			),
			FieldSwapRule::ID          => array(
				'enabled' => true,
				'points'  => FieldSwapRule::DEFAULT_POINTS,
			),
			TokenMissingRule::ID       => array(
				'enabled' => true,
				'points'  => TokenMissingRule::DEFAULT_POINTS,
			),
			TokenExpiredRule::ID       => array(
				'enabled' => true,
				'points'  => TokenExpiredRule::DEFAULT_POINTS,
			),
			JsTimestampRule::ID        => array(
				'enabled' => true,
				'points'  => JsTimestampRule::DEFAULT_POINTS,
			),
			FillTimeRule::ID           => array(
				'enabled'     => true,
				'points'      => FillTimeRule::DEFAULT_POINTS,
				'min_seconds' => FillTimeRule::MIN_SECONDS,
			),
			AcceptLanguageRule::ID     => array(
				'enabled' => true,
				'points'  => AcceptLanguageRule::DEFAULT_POINTS,
			),
			UaLibraryRule::ID          => array(
				'enabled' => true,
				'points'  => UaLibraryRule::DEFAULT_POINTS,
			),
			UaEmptyRule::ID            => array(
				'enabled' => true,
				'points'  => UaEmptyRule::DEFAULT_POINTS,
			),
			SecFetchRule::ID           => array(
				'enabled' => true,
				'points'  => SecFetchRule::DEFAULT_POINTS,
			),
			ProtocolRule::ID           => array(
				'enabled' => true,
				'points'  => ProtocolRule::DEFAULT_POINTS,
			),
			RateLimitRule::ID          => array(
				'enabled' => true,
				'points'  => RateLimitRule::DEFAULT_POINTS,
				'max'     => RateLimitRule::DEFAULT_MAX,
				'window'  => RateLimitRule::DEFAULT_WINDOW,
			),
			BbcodeRule::ID             => array(
				'enabled' => true,
				'points'  => BbcodeRule::DEFAULT_POINTS,
			),
			LinkCountRule::ID          => array(
				'enabled' => true,
				'points'  => LinkCountRule::DEFAULT_POINTS,
			),
			LinkRatioRule::ID          => array(
				'enabled' => true,
				'points'  => LinkRatioRule::DEFAULT_POINTS,
			),
			UrlOnlyRule::ID            => array(
				'enabled' => true,
				'points'  => UrlOnlyRule::DEFAULT_POINTS,
			),
			ShortenerRule::ID          => array(
				'enabled' => true,
				'points'  => ShortenerRule::DEFAULT_POINTS,
			),
			CheapTldRule::ID           => array(
				'enabled' => true,
				'points'  => CheapTldRule::DEFAULT_POINTS,
			),
			ForeignScriptRule::ID      => array(
				'enabled' => true,
				'points'  => ForeignScriptRule::DEFAULT_POINTS,
			),
			HomoglyphRule::ID          => array(
				'enabled' => true,
				'points'  => HomoglyphRule::DEFAULT_POINTS,
			),
			PhraseSetRule::ID          => array(
				'enabled' => true,
				'points'  => PhraseSetRule::DEFAULT_POINTS,
			),
			AuthorStructureRule::ID    => array(
				'enabled' => true,
				'points'  => AuthorStructureRule::DEFAULT_POINTS,
			),
			ConsonantRule::ID          => array(
				'enabled' => true,
				'points'  => ConsonantRule::DEFAULT_POINTS,
			),
			DisposableEmailRule::ID    => array(
				'enabled'        => true,
				'points'         => DisposableEmailRule::DEFAULT_POINTS,
				'points_comment' => DisposableEmailRule::DEFAULT_POINTS_COMMENT,
			),
			RepeatOffenderRule::ID     => array(
				'enabled' => true,
				'points'  => RepeatOffenderRule::DEFAULT_POINTS,
			),
			CommentLocaldbRule::ID     => array(
				'enabled' => true,
				'points'  => CommentLocaldbRule::DEFAULT_POINTS,
			),
			CustomKeywordRule::ID      => array(
				'enabled' => true,
				'points'  => CustomKeywordRule::DEFAULT_POINTS,
			),
			DisallowedKeysRule::ID     => array(
				'enabled' => true,
				'points'  => DisallowedKeysRule::DEFAULT_POINTS,
			),
		);
	}

	/**
	 * Baut den aktiven Regelsatz (nur eingeschaltete Regeln, Punkte aus den
	 * Settings-Overrides).
	 *
	 * @return array<int, RuleInterface>
	 */
	public function build(): array {
		$rules = array();

		$candidates = array(
			TrustApprovedEmailRule::ID => fn( int $p ) => new TrustApprovedEmailRule( $p ),
			TrustUserHistoryRule::ID   => fn( int $p ) => new TrustUserHistoryRule( $p ),
			TrustVerifiedBuyerRule::ID => fn( int $p ) => new TrustVerifiedBuyerRule( $p ),
			TrustAllowlistRule::ID     => fn( int $p ) => new TrustAllowlistRule( $p ),
			HoneypotRule::ID           => fn( int $p ) => new HoneypotRule( $p ),
			FieldSwapRule::ID          => fn( int $p ) => new FieldSwapRule( $p ),
			TokenMissingRule::ID       => fn( int $p ) => new TokenMissingRule( $this->tokens, $p ),
			TokenExpiredRule::ID       => fn( int $p ) => new TokenExpiredRule( $this->tokens, $p ),
			JsTimestampRule::ID        => fn( int $p ) => new JsTimestampRule( $p ),
			FillTimeRule::ID           => fn( int $p ) => new FillTimeRule( $p, (int) $this->option( FillTimeRule::ID, 'min_seconds', FillTimeRule::MIN_SECONDS ) ),
			AcceptLanguageRule::ID     => fn( int $p ) => new AcceptLanguageRule( $p ),
			UaLibraryRule::ID          => fn( int $p ) => new UaLibraryRule( $p ),
			UaEmptyRule::ID            => fn( int $p ) => new UaEmptyRule( $p ),
			SecFetchRule::ID           => fn( int $p ) => new SecFetchRule( $p ),
			ProtocolRule::ID           => fn( int $p ) => new ProtocolRule( $p ),
			RateLimitRule::ID          => fn( int $p ) => new RateLimitRule(
				$this->hasher,
				$p,
				(int) $this->option( RateLimitRule::ID, 'max', RateLimitRule::DEFAULT_MAX ),
				(int) $this->option( RateLimitRule::ID, 'window', RateLimitRule::DEFAULT_WINDOW )
			),
			BbcodeRule::ID             => fn( int $p ) => new BbcodeRule( $p ),
			LinkCountRule::ID          => fn( int $p ) => new LinkCountRule( $p ),
			LinkRatioRule::ID          => fn( int $p ) => new LinkRatioRule( $p ),
			UrlOnlyRule::ID            => fn( int $p ) => new UrlOnlyRule( $p ),
			ShortenerRule::ID          => fn( int $p ) => new ShortenerRule( Lists::shortener_domains(), $p ),
			CheapTldRule::ID           => fn( int $p ) => new CheapTldRule( Lists::cheap_tlds(), $p ),
			ForeignScriptRule::ID      => fn( int $p ) => new ForeignScriptRule( $this->foreign_scripts(), $p ),
			HomoglyphRule::ID          => fn( int $p ) => new HomoglyphRule( $p ),
			PhraseSetRule::ID          => fn( int $p ) => new PhraseSetRule( Lists::phrase_sets(), $p ),
			AuthorStructureRule::ID    => fn( int $p ) => new AuthorStructureRule( $p ),
			ConsonantRule::ID          => fn( int $p ) => new ConsonantRule( $p ),
			DisposableEmailRule::ID    => fn( int $p ) => new DisposableEmailRule(
				Lists::disposable_domains(),
				$p,
				(int) $this->option( DisposableEmailRule::ID, 'points_comment', DisposableEmailRule::DEFAULT_POINTS_COMMENT )
			),
			RepeatOffenderRule::ID     => fn( int $p ) => null !== $this->log ? new RepeatOffenderRule( $this->log, $this->hasher, $p ) : null,
			CommentLocaldbRule::ID     => fn( int $p ) => new CommentLocaldbRule( $this->comments, $p ),
			CustomKeywordRule::ID      => fn( int $p ) => new CustomKeywordRule( $this->custom_keywords(), $p ),
			DisallowedKeysRule::ID     => fn( int $p ) => new DisallowedKeysRule( $p ),
		);

		$defaults = self::default_rule_settings();

		foreach ( $candidates as $id => $factory ) {
			if ( ! $this->enabled( $id ) ) {
				continue;
			}
			$points = (int) $this->option( $id, 'points', (int) ( $defaults[ $id ]['points'] ?? 0 ) );
			$rule   = $factory( $points );
			if ( $rule instanceof RuleInterface ) {
				$rules[] = $rule;
			}
		}

		if ( $this->pro ) {
			foreach ( $this->pro_rules() as $rule ) {
				$rules[] = $rule;
			}
		}

		return $rules;
	}

	/**
	 * Pro-Regeln (Stufe 3): Bayes und eskalierender Proof-of-Work, jeweils per
	 * eigenem Setting schaltbar. Nur bei aktivem Pro-Gate aufgerufen.
	 *
	 * @return array<int, RuleInterface>
	 */
	protected function pro_rules(): array {
		$pro   = is_array( $this->settings['pro'] ?? null ) ? $this->settings['pro'] : array();
		$rules = array();

		$bayes = is_array( $pro['bayes'] ?? null ) ? $pro['bayes'] : array();
		if ( ! empty( $bayes['enabled'] ?? true ) ) {
			$rules[] = new BayesRule(
				new Tokenizer(),
				new Classifier(),
				new TokenRepository(),
				(int) ( $bayes['ham_messages'] ?? 0 ),
				(int) ( $bayes['spam_messages'] ?? 0 )
			);
		}

		$pow = is_array( $pro['pow'] ?? null ) ? $pro['pow'] : array();
		if ( ! empty( $pow['enabled'] ?? false ) ) {
			$rules[] = new PowRule( new PowChallenge(), $this->hasher );
		}

		foreach ( $this->externa_rules( $pro ) as $rule ) {
			$rules[] = $rule;
		}

		return $rules;
	}

	/**
	 * W5b Opt-in-Externa (Stufe 3): DB-IP GeoIP/ASN, AbuseIPDB, KI-Zweitmeinung.
	 * Jedes Feature registriert sich NUR bei eigenem Opt-in (Default aus). Ohne
	 * Opt-in wird die Regel gar nicht erst gebaut - kein Byte verlaesst die Site.
	 *
	 * @param array<string, mixed> $pro Pro-Settings-Zweig.
	 * @return array<int, RuleInterface>
	 */
	protected function externa_rules( array $pro ): array {
		$rules = array();

		// DB-IP GeoIP/ASN: Land (konfigurierbare Liste, Default leer) + Datacenter-ASN.
		$geoip = is_array( $pro['geoip'] ?? null ) ? $pro['geoip'] : array();
		if ( ! empty( $geoip['enabled'] ?? false ) ) {
			$geo       = new GeoDatabase(
				Uploads::path( GeoDownloader::COUNTRY_FILE ),
				Uploads::path( GeoDownloader::ASN_FILE )
			);
			$countries = is_array( $geoip['countries'] ?? null ) ? $geoip['countries'] : array();
			$rules[]   = new CountryRule( $geo, $countries );
			$rules[]   = new AsnDatacenterRule( $geo, Lists::datacenter_asns() );
		}

		// AbuseIPDB: BYO-Key (via Core-Secrets entschluesselt).
		$abuse = is_array( $pro['abuseipdb'] ?? null ) ? $pro['abuseipdb'] : array();
		if ( ! empty( $abuse['enabled'] ?? false ) && '' !== (string) ( $abuse['key_encrypted'] ?? '' ) ) {
			$key     = Secrets::decrypt( (string) $abuse['key_encrypted'] );
			$rules[] = new AbuseIpdbRule( new AbuseIpdbClient( $key ), $this->hasher );
		}

		// KI-Zweitmeinung: eigenes Opt-in NEBEN dem Core-Hauptschalter; nutzt die
		// Core-KI-Fassade (H1), kein eigener HTTP-Client.
		$ai = is_array( $pro['ai'] ?? null ) ? $pro['ai'] : array();
		if ( ! empty( $ai['enabled'] ?? false ) ) {
			$rules[] = new AiSecondOpinionRule( Core::instance()->ai(), true );
		}

		return $rules;
	}

	/**
	 * Ist eine Regel eingeschaltet (Default: ja)?
	 *
	 * @param string $id Regel-ID.
	 * @return bool
	 */
	protected function enabled( string $id ): bool {
		$value = $this->settings['rules'][ $id ]['enabled'] ?? true;
		return (bool) $value;
	}

	/**
	 * Regel-Option mit Fallback.
	 *
	 * @param string $id      Regel-ID.
	 * @param string $key     Options-Schluessel.
	 * @param mixed  $default Fallback.
	 * @return mixed
	 */
	protected function option( string $id, string $key, $default ) {
		return $this->settings['rules'][ $id ][ $key ] ?? $default;
	}

	/**
	 * Konfigurierte fremde Schriftsysteme (Settings lists.foreign_scripts).
	 *
	 * @return array<int, string>
	 */
	protected function foreign_scripts(): array {
		$scripts = $this->settings['lists']['foreign_scripts'] ?? array( 'Cyrillic', 'Han', 'Arabic', 'Hebrew', 'Thai' );
		return is_array( $scripts ) ? array_values( array_map( 'strval', $scripts ) ) : array();
	}

	/**
	 * Eigene Keyword-Liste des Betreibers (Settings lists.custom_keywords).
	 *
	 * @return array<int, string>
	 */
	protected function custom_keywords(): array {
		$keywords = $this->settings['lists']['custom_keywords'] ?? array();
		return is_array( $keywords ) ? array_values( array_map( 'strval', $keywords ) ) : array();
	}
}
