<?php

declare(strict_types=1);

namespace SiteFort\Modules\Firewall;

use SiteFort\Includes\SettingsStore;
use SiteFort\Includes\IpDetector;
use SiteFort\Includes\ProxyPresets;

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

final class FirewallConfigService {
	public const MODE_DISABLED           = 'disabled';
	public const MODE_PENDING_VALIDATION = 'pending_validation';
	public const MODE_MONITOR            = 'monitor';
	public const MODE_ENFORCE            = 'enforce';
	private const RUNTIME_READINESS_MAX_AGE = 30 * MINUTE_IN_SECONDS;
	private const EFFECTIVE_MODE_STATE_OPTION = 'sitefort_firewall_effective_mode_state';
	private const EFFECTIVE_MODE_STATE_VERSION = 2;

	public const CONTEXT_INTERACTIVE    = 'interactive_http';
	public const CONTEXT_NON_INTERACTIVE = 'non_interactive';

	/**
	 * @return string[]
	 */
	public static function internal_keys(): array {
		return [ 'origin_enforcement_mode' ];
	}

	/**
	 * @param array<int,string> $extra_locked_keys
	 * @return array<int,string>
	 */
	public static function locked_patch_keys( array $extra_locked_keys = [] ): array {
		return array_values(
			array_unique(
				array_merge( self::internal_keys(), $extra_locked_keys )
			)
		);
	}

	public static function normalize_origin_enforcement_mode( $mode, bool $enabled ): string {
		$mode = sanitize_key( (string) $mode );

		if ( ! $enabled ) {
			return self::MODE_DISABLED;
		}

		if ( in_array( $mode, [ self::MODE_PENDING_VALIDATION, self::MODE_MONITOR, self::MODE_ENFORCE ], true ) ) {
			return $mode;
		}

		return self::MODE_PENDING_VALIDATION;
	}

	/**
	 * @param array<string,mixed> $current
	 * @param array<string,mixed> $patch
	 * @param array<int,string>   $locked_keys
	 * @return array{settings: array<string,mixed>, readiness: ?array<string,mixed>}
	 */
	public static function apply_patch( array $current, array $patch, array $locked_keys, string $context ): array {
		$settings = FirewallSettings::apply_patch( $current, $patch, self::locked_patch_keys( $locked_keys ) );
		FirewallSettings::assert_supported_configuration( $settings );

		return self::apply_contextual_mode( $settings, $context );
	}

	/**
	 * @param array<string,mixed> $settings
	 * @return array{settings: array<string,mixed>, readiness: ?array<string,mixed>}
	 */
	public static function prepare_imported_settings( array $settings, string $context ): array {
		$settings = FirewallSettings::sanitize( $settings );
		FirewallSettings::assert_supported_configuration( $settings );

		return self::apply_contextual_mode( $settings, $context );
	}

	/**
	 * @param array<string,mixed> $settings Sanitized firewall settings.
	 * @return array{settings: array<string,mixed>, readiness: ?array<string,mixed>, mode: string, changed: bool, enforce: bool}
	 */
	public static function reconcile_runtime_mode_from_sanitized( array $settings ): array {
		$current  = self::get_origin_enforcement_mode( $settings );

		if ( ! empty( $settings['enabled'] ) && ! FirewallBypass::should_skip_origin_validation() ) {
			$readiness = self::get_runtime_readiness( $settings );
			$desired   = self::mode_from_readiness( $readiness );

			if ( $current !== $desired ) {
				$settings['origin_enforcement_mode'] = $desired;
			}

			return [
				'settings' => $settings,
				'readiness' => $readiness,
				'mode' => $desired,
				'changed' => $current !== $desired,
				'enforce' => $desired === self::MODE_ENFORCE,
			];
		}

		$desired = self::normalize_origin_enforcement_mode( $settings['origin_enforcement_mode'] ?? '', ! empty( $settings['enabled'] ) );
		$settings['origin_enforcement_mode'] = $desired;

		return [
			'settings' => $settings,
			'readiness' => null,
			'mode' => $desired,
			'changed' => $current !== $desired,
			'enforce' => $desired === self::MODE_ENFORCE,
		];
	}

	/**
	 * Resolve origin enforcement for public runtime without recomputing
	 * readiness or persisting effective-mode state.
	 *
	 * @param array<string,mixed> $settings Sanitized firewall settings.
	 * @return array{settings: array<string,mixed>, readiness: ?array<string,mixed>, mode: string, changed: bool, enforce: bool}
	 */
	public static function resolve_runtime_mode_from_cached_state( array $settings ): array {
		$current = self::get_origin_enforcement_mode( $settings );

		if ( empty( $settings['enabled'] ) ) {
			$settings['origin_enforcement_mode'] = self::MODE_DISABLED;

			return [
				'settings' => $settings,
				'readiness' => null,
				'mode' => self::MODE_DISABLED,
				'changed' => $current !== self::MODE_DISABLED,
				'enforce' => false,
			];
		}

		if ( ! FirewallBypass::should_skip_origin_validation() ) {
			$settings = self::apply_persisted_effective_mode( $settings );
		}

		$desired = self::get_origin_enforcement_mode( $settings );
		$settings['origin_enforcement_mode'] = $desired;

		return [
			'settings' => $settings,
			'readiness' => FirewallBypass::should_skip_origin_validation()
				? null
				: self::get_effective_readiness_snapshot( $settings ),
			'mode' => $desired,
			'changed' => $current !== $desired,
			'enforce' => $desired === self::MODE_ENFORCE,
		];
	}

	/**
	 * @param array<string,mixed> $settings
	 */
	public static function get_origin_enforcement_mode( array $settings ): string {
		return self::normalize_origin_enforcement_mode(
			$settings['origin_enforcement_mode'] ?? '',
			! empty( $settings['enabled'] )
		);
	}

	/**
	 * @param array<string,mixed> $settings
	 */
	public static function is_origin_enforcement_active( array $settings ): bool {
		return self::get_origin_enforcement_mode( $settings ) === self::MODE_ENFORCE;
	}

	/**
	 * Load firewall settings and actively reconcile the effective origin mode.
	 *
	 * This method is for admin/operator-owned contexts that may refresh
	 * readiness and sync the effective-mode option. Public enforcement must use
	 * load_runtime_effective_settings() instead.
	 *
	 * @return array<string,mixed>
	 */
	public static function load_effective_settings(): array {
		$settings = FirewallSettings::get();

		if ( empty( $settings['enabled'] ) ) {
			return $settings;
		}

		if ( FirewallBypass::should_skip_origin_validation() ) {
			return self::apply_persisted_effective_mode( $settings );
		}

		$cached = get_transient( IpDetector::READINESS_TRANSIENT );
		$cached_at = is_array( $cached ) ? (int) ( $cached['cached_at'] ?? 0 ) : 0;
		$signature_matches = is_array( $cached )
			&& IpDetector::is_readiness_signature_compatible( $cached, $settings );
		$age = $cached_at > 0 ? ( time() - $cached_at ) : PHP_INT_MAX;

		if ( ! $signature_matches || $age > self::RUNTIME_READINESS_MAX_AGE ) {
			IpDetector::refresh_readiness( $settings );
		}

		$state = self::reconcile_runtime_mode_from_sanitized( $settings );
		self::sync_effective_mode_state(
			$state['settings'],
			(string) ( $state['mode'] ?? self::MODE_PENDING_VALIDATION ),
			is_array( $state['readiness'] ?? null ) ? $state['readiness'] : null
		);

		return $state['settings'];
	}

	/**
	 * Load firewall settings for public runtime consumers. This method is
	 * intentionally read-only: it never refreshes readiness and never writes the
	 * effective-mode option.
	 *
	 * @return array<string,mixed>
	 */
	public static function load_runtime_effective_settings(): array {
		$settings = FirewallSettings::get();

		if ( empty( $settings['enabled'] ) ) {
			return $settings;
		}

		$state = self::resolve_runtime_mode_from_cached_state( FirewallSettings::sanitize( $settings ) );

		return $state['settings'];
	}

	/**
	 * Persist the last interactive origin-enforcement verdict without rewriting
	 * the full firewall settings blob.
	 *
	 * @param array<string,mixed>      $settings  Sanitized firewall settings.
	 * @param array<string,mixed>|null $readiness Interactive readiness payload.
	 */
	public static function sync_effective_mode_state( array $settings, string $mode, ?array $readiness = null ): void {
		if ( empty( $settings['enabled'] ) ) {
			delete_option( self::EFFECTIVE_MODE_STATE_OPTION );
			return;
		}

		if ( FirewallBypass::should_skip_origin_validation() ) {
			return;
		}

		$payload = [
			'version' => self::EFFECTIVE_MODE_STATE_VERSION,
			'mode' => self::normalize_origin_enforcement_mode( $mode, true ),
			'readiness_signature' => self::build_effective_mode_readiness_signature( $settings ),
			'readiness' => self::sanitize_effective_mode_readiness( $readiness ),
		];

		$existing = get_option( self::EFFECTIVE_MODE_STATE_OPTION, null );
		if ( is_array( $existing ) && self::effective_mode_state_matches( $existing, $payload ) ) {
			return;
		}

		update_option( self::EFFECTIVE_MODE_STATE_OPTION, $payload, false );
	}

	/**
	 * Persist firewall settings from a non-interactive context.
	 *
	 * These writes must never trust the current request IP. When they change the
	 * IP-readiness inputs, the stored effective mode is reset to pending
	 * validation after the row save succeeds. A second WAF refresh is
	 * intentional here: the first save hook may have refreshed bootstrap config
	 * using the previous effective-mode state.
	 *
	 * @param array<string,mixed> $settings
	 */
	public static function persist_non_interactive_settings( array $settings ): bool {
		$settings = FirewallSettings::sanitize( $settings );
		$current_settings = FirewallSettings::get();
		$readiness_changed =
			self::build_effective_mode_readiness_signature( $current_settings )
			!== self::build_effective_mode_readiness_signature( $settings );

		if ( ! SettingsStore::save( SettingsStore::COL_FIREWALL, FirewallSettings::prepare_for_storage( $settings ) ) ) {
			return false;
		}

		if ( ! $readiness_changed ) {
			return true;
		}

		self::sync_pending_validation_state( $settings );

		if ( class_exists( '\SiteFort\Modules\Firewall\ExtendedProtection\ConfigCoordinator' ) ) {
			\SiteFort\Modules\Firewall\ExtendedProtection\ConfigCoordinator::ensure_runtime_assets_current();
		}

		return true;
	}

	/**
	 * Return the readiness snapshot that matches the effective mode for the
	 * current request context.
	 *
	 * @param array<string,mixed> $settings Sanitized firewall settings.
	 * @return array<string,mixed>|null
	 */
	public static function get_effective_readiness_snapshot( array $settings ): ?array {
		if ( empty( $settings['enabled'] ) ) {
			return null;
		}

		$state = self::get_persisted_effective_mode_state( $settings );
		if ( is_array( $state ) && is_array( $state['readiness'] ?? null ) ) {
			return $state['readiness'];
		}

		if ( FirewallBypass::should_skip_origin_validation() ) {
			return null;
		}

		$cached = get_transient( IpDetector::READINESS_TRANSIENT );
		if (
			is_array( $cached )
			&& self::is_cached_readiness_fresh( $cached )
			&& IpDetector::is_readiness_signature_compatible( $cached, $settings )
		) {
			return self::sanitize_effective_mode_readiness( $cached );
		}

		return null;
	}

	/**
	 * Build a canonical readiness payload for remote/non-interactive writes.
	 *
	 * @return array<string,mixed>
	 */
	public static function build_pending_validation_readiness(): array {
		return [
			'ready'     => false,
			'severity'  => 'unknown',
			'reason'    => 'Visitor IP detection has not been revalidated from a real site request since the latest remote firewall configuration change.',
			'action'    => 'Open the site or firewall settings page so SiteFort can verify the real visitor IP source before local blocking begins.',
			'cached_at' => time(),
		];
	}

	/**
	 * @param array<string,mixed> $settings
	 * @return array{settings: array<string,mixed>, readiness: ?array<string,mixed>}
	 */
	private static function apply_contextual_mode( array $settings, string $context ): array {
		$settings = FirewallSettings::sanitize( $settings );

		if ( empty( $settings['enabled'] ) ) {
			$settings['origin_enforcement_mode'] = self::MODE_DISABLED;

			return [
				'settings' => $settings,
				'readiness' => null,
			];
		}

		if ( $context !== self::CONTEXT_INTERACTIVE ) {
			$settings['origin_enforcement_mode'] = self::MODE_PENDING_VALIDATION;

			return [
				'settings' => $settings,
				'readiness' => null,
			];
		}

		$settings = self::normalize_verified_proxy_context( $settings );
		$readiness = IpDetector::refresh_readiness( $settings );
		$settings['origin_enforcement_mode'] = self::mode_from_readiness( $readiness );

		return [
			'settings' => $settings,
			'readiness' => $readiness,
		];
	}

	/**
	 * Normalize verified proxy settings from the real interactive request.
	 *
	 * If the admin explicitly selects Cloudflare's CF-Connecting-IP header while
	 * the current request is verifiably coming from Cloudflare, persist the
	 * Cloudflare trusted-proxy preset as well. This keeps runtime detection,
	 * readiness, pre-WP WAF, and UI state aligned instead of relying on one
	 * special-case header path.
	 *
	 * @param array<string,mixed> $settings
	 * @return array<string,mixed>
	 */
	private static function normalize_verified_proxy_context( array $settings ): array {
		$mode   = (string) ( $settings['proxy_detection'] ?? 'automatic' );
		$header = (string) ( $settings['proxy_header'] ?? '' );
		$preset = (string) ( $settings['trusted_proxy_preset'] ?? ProxyPresets::PRESET_NONE );

		if (
			'manual' === $mode
			&& 'HTTP_CF_CONNECTING_IP' === $header
			&& ProxyPresets::PRESET_NONE === $preset
			&& IpDetector::is_current_request_cloudflare_connection()
		) {
			$settings['trusted_proxy_preset'] = ProxyPresets::PRESET_CLOUDFLARE;
		}

		return $settings;
	}

	/**
	 * @return array<string,mixed>
	 */
	private static function get_runtime_readiness( array $settings ): array {
		$cached = get_transient( IpDetector::READINESS_TRANSIENT );
		if (
			is_array( $cached )
			&& self::is_cached_readiness_fresh( $cached )
			&& IpDetector::is_readiness_signature_compatible( $cached, $settings )
		) {
			return $cached;
		}

		return IpDetector::refresh_readiness( $settings );
	}

	/**
	 * @param array<string,mixed> $settings Sanitized firewall settings.
	 * @return array<string,mixed>
	 */
	private static function apply_persisted_effective_mode( array $settings ): array {
		$state = self::get_persisted_effective_mode_state( $settings );
		if ( is_array( $state ) && ! empty( $state['mode'] ) ) {
			$settings['origin_enforcement_mode'] = (string) $state['mode'];
			return $settings;
		}

		$cached = get_transient( IpDetector::READINESS_TRANSIENT );
		if (
			is_array( $cached )
			&& self::is_cached_readiness_fresh( $cached )
			&& IpDetector::is_readiness_signature_compatible( $cached, $settings )
		) {
			$settings['origin_enforcement_mode'] = self::mode_from_readiness( $cached );
			return $settings;
		}

		$settings['origin_enforcement_mode'] = self::get_origin_enforcement_mode( $settings );

		return $settings;
	}

	/**
	 * @param array<string,mixed> $settings Sanitized firewall settings.
	 * @return array<string,mixed>|null
	 */
	private static function get_persisted_effective_mode_state( array $settings ): ?array {
		$state = get_option( self::EFFECTIVE_MODE_STATE_OPTION, null );
		if ( ! is_array( $state ) ) {
			return null;
		}

		if ( (int) ( $state['version'] ?? 0 ) !== self::EFFECTIVE_MODE_STATE_VERSION ) {
			return null;
		}

		$current_signature = self::build_effective_mode_readiness_signature( $settings );
		$stored_signature = (string) ( $state['readiness_signature'] ?? '' );
		if ( $stored_signature === '' || ! hash_equals( $stored_signature, $current_signature ) ) {
			return null;
		}

		$mode = self::normalize_origin_enforcement_mode( (string) ( $state['mode'] ?? '' ), true );

		return [
			'mode' => $mode,
			'readiness' => self::sanitize_effective_mode_readiness( is_array( $state['readiness'] ?? null ) ? $state['readiness'] : null ),
		];
	}

	/**
	 * @param array<string,mixed> $settings Sanitized firewall settings.
	 */
	private static function build_effective_mode_readiness_signature( array $settings ): string {
		$payload = wp_json_encode(
			[
				'enabled' => ! empty( $settings['enabled'] ),
				'readiness_signature' => IpDetector::get_readiness_signature( $settings ),
			],
			JSON_UNESCAPED_SLASHES
		);

		if ( ! is_string( $payload ) ) {
			$payload = serialize(
				[
					'enabled' => ! empty( $settings['enabled'] ),
					'readiness_signature' => IpDetector::get_readiness_signature( $settings ),
				]
			);
		}

		return hash( 'sha256', $payload );
	}

	/**
	 * Force a non-interactive firewall write back into pending validation.
	 *
	 * @param array<string,mixed> $settings Sanitized firewall settings.
	 */
	private static function sync_pending_validation_state( array $settings ): void {
		if ( empty( $settings['enabled'] ) ) {
			delete_option( self::EFFECTIVE_MODE_STATE_OPTION );
			return;
		}

		$settings['origin_enforcement_mode'] = self::MODE_PENDING_VALIDATION;
		$payload = [
			'version' => self::EFFECTIVE_MODE_STATE_VERSION,
			'mode' => self::MODE_PENDING_VALIDATION,
			'readiness_signature' => self::build_effective_mode_readiness_signature( $settings ),
			'readiness' => self::sanitize_effective_mode_readiness( self::build_pending_validation_readiness() ),
		];

		$existing = get_option( self::EFFECTIVE_MODE_STATE_OPTION, null );
		if ( is_array( $existing ) && self::effective_mode_state_matches( $existing, $payload ) ) {
			return;
		}

		update_option( self::EFFECTIVE_MODE_STATE_OPTION, $payload, false );
	}

	/**
	 * @param array<string,mixed>|null $readiness
	 * @return array<string,mixed>|null
	 */
	private static function sanitize_effective_mode_readiness( ?array $readiness ): ?array {
		if ( ! is_array( $readiness ) || ! array_key_exists( 'ready', $readiness ) ) {
			return null;
		}

		return [
			'ready' => ! empty( $readiness['ready'] ),
			'severity' => (string) ( $readiness['severity'] ?? '' ),
			'reason' => (string) ( $readiness['reason'] ?? '' ),
			'action' => (string) ( $readiness['action'] ?? '' ),
			'detected_ip' => (string) ( $readiness['detected_ip'] ?? '' ),
			'remote_addr' => (string) ( $readiness['remote_addr'] ?? '' ),
			'probe_status' => (string) ( $readiness['probe_status'] ?? '' ),
			'cached_at' => (int) ( $readiness['cached_at'] ?? 0 ),
		];
	}

	/**
	 * @param array<string,mixed> $existing
	 * @param array<string,mixed> $expected
	 */
	private static function effective_mode_state_matches( array $existing, array $expected ): bool {
		return (int) ( $existing['version'] ?? 0 ) === (int) $expected['version']
			&& (string) ( $existing['mode'] ?? '' ) === (string) $expected['mode']
			&& (string) ( $existing['readiness_signature'] ?? '' ) === (string) $expected['readiness_signature']
			&& self::sanitize_effective_mode_readiness( is_array( $existing['readiness'] ?? null ) ? $existing['readiness'] : null )
				== self::sanitize_effective_mode_readiness( is_array( $expected['readiness'] ?? null ) ? $expected['readiness'] : null );
	}

	/**
	 * @param array<string,mixed> $readiness
	 */
	private static function is_cached_readiness_fresh( array $readiness ): bool {
		$cached_at = (int) ( $readiness['cached_at'] ?? 0 );
		if ( $cached_at <= 0 ) {
			return false;
		}

		return ( time() - $cached_at ) <= self::RUNTIME_READINESS_MAX_AGE;
	}

	/**
	 * @param array<string,mixed> $readiness
	 */
	private static function mode_from_readiness( array $readiness ): string {
		if ( ! empty( $readiness['ready'] ) ) {
			return self::MODE_ENFORCE;
		}

		return ( $readiness['severity'] ?? '' ) === 'unknown'
			? self::MODE_PENDING_VALIDATION
			: self::MODE_MONITOR;
	}
}
