<?php

declare(strict_types=1);

namespace SiteFort\Modules\Firewall\SearchTrust;

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

use SiteFort\Includes\IpDetector;
use SiteFort\Modules\Firewall\SafeNetwork\SafeNetworkRanges;

final class SearchTrustRuntimeResolver {
	public const STATUS_NONE = 'none';
	public const STATUS_INELIGIBLE = 'ineligible';

	private const CACHE_GROUP = 'sitefort_search_trust';
	private const LOCK_TTL = 30;

	private const MAX_DNS_LOOKUPS_PER_REQUEST = 1;
	private const DNS_BREAKER_TRANSIENT = 'sitefort_search_trust_dns_breaker';
	private const DNS_BREAKER_TTL = 300;
	private const DNS_SLOW_THRESHOLD = 1.5;

	/** Resolver calls spent by this request. */
	private static int $dns_lookups_this_request = 0;

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

	/** @var array<string, string> */
	private array $enforcement_status_cache = [];

	/** @var array<string, mixed>|null */
	private ?array $current_context = null;

	/** @var array{protected_paths: array<int, string>, protected_prefixes: array<int, string>, scope_hash: string}|null */
	private ?array $current_scope = null;

	/**
	 * @param array<string, mixed> $settings
	 */
	public function __construct( array $settings ) {
		$this->settings = $settings;
	}

	/**
	 * @param array<string, mixed> $request
	 * @return array<string, mixed>
	 */
	public function resolve_current_request(
		string $ip,
		string $user_agent,
		bool $pre_wordpress_waf_active,
		bool $country_check_active,
		bool $community_check_active,
		array $request = []
	): array {
		if ( $this->current_context !== null ) {
			return $this->current_context;
		}

		$scope = $this->get_current_scope();
		$scope_hash = (string) ( $scope['scope_hash'] ?? '' );
		$bootstrap_context = $pre_wordpress_waf_active ? SearchTrustContext::from_bootstrap_constant() : null;
		if (
			$bootstrap_context !== null
			&& SearchTrustContext::matches_scope_hash( $bootstrap_context, $scope_hash )
		) {
			$this->current_context = $bootstrap_context;
			return $this->current_context;
		}

		if (
			! SearchTrustCore::should_resolve_verified_search_exception(
				! empty( $this->settings['rate_limit_enabled'] ),
				$country_check_active,
				$community_check_active,
				\SiteFort\Modules\Firewall\RuleEngine::should_verify_search_engine_claim(
					! empty( $this->settings['bot_policy_enabled'] ),
					(string) ( $this->settings['bot_policy_profile'] ?? '' )
				)
			)
		) {
			$this->current_context = SearchTrustContext::create(
				[
					'scope_hash' => $scope_hash,
				]
			);
			return $this->current_context;
		}

		$engine = SearchTrustCore::get_verified_search_engine_claim( $user_agent ) ?? '';
		if ( $engine === '' ) {
			$this->current_context = SearchTrustContext::create(
				[
					'scope_hash' => $scope_hash,
				]
			);
			return $this->current_context;
		}

		$eligible = SearchTrustCore::is_verified_search_request_eligible(
			[
				'method' => (string) ( $request['method'] ?? IpDetector::get_server_value( 'REQUEST_METHOD' ) ),
				'request_uri' => (string) ( $request['request_uri'] ?? IpDetector::get_server_value( 'REQUEST_URI' ) ),
			],
			$scope
		);

		if ( ! $eligible ) {
			$this->current_context = SearchTrustContext::create(
				[
					'engine' => $engine,
					'eligible' => false,
					'scope_hash' => $scope_hash,
				]
			);
			return $this->current_context;
		}

		$status = $this->classify_claimed_engine( $ip, $engine );
		$this->enforcement_status_cache[ $engine . '|' . SearchTrustCore::normalize_ip( $ip ) ] = $status;

		$this->current_context = SearchTrustContext::create(
			[
				'checked' => true,
				'eligible' => true,
				'verified' => $status === SearchTrustDnsVerifier::TRUST_VERIFIED,
				'exception' => $status === SearchTrustDnsVerifier::TRUST_VERIFIED,
				'engine' => $engine,
				'scope_hash' => $scope_hash,
				'status' => $status,
			]
		);

		return $this->current_context;
	}

	public function classify_claimed_engine( string $ip, string $engine ): string {
		return SearchTrustDnsVerifier::classify(
			$ip,
			$engine,
			function ( string $cache_key ): ?array {
				return $this->get_cached_verified_search_result( $cache_key );
			},
			function ( string $cache_key, bool $verified_result, int $ttl ): void {
				$this->cache_verified_search_result( $cache_key, $verified_result, $ttl );
			},
			function ( string $cache_key, array $record ): void {
				$this->touch_verified_search_result( $cache_key, $record );
			},
			function ( string $cache_key ) {
				return $this->acquire_verified_search_lock( $cache_key );
			},
			function ( $lock_token ): void {
				$this->release_verified_search_lock( $lock_token );
			},
			static function ( string $candidate_ip, string $candidate_engine ): bool {
				return SafeNetworkRanges::is_ip_in_set( $candidate_ip, $candidate_engine );
			},
			static function (): bool {
				return self::allow_dns_lookup();
			},
			static function ( float $seconds ): void {
				self::record_dns_duration( $seconds );
			}
		);
	}

	// One lookup per request, plus a shared breaker that pauses after a slow one.
	private static function allow_dns_lookup(): bool {
		if ( self::$dns_lookups_this_request >= self::MAX_DNS_LOOKUPS_PER_REQUEST ) {
			return false;
		}

		if ( get_transient( self::DNS_BREAKER_TRANSIENT ) ) {
			return false;
		}

		self::$dns_lookups_this_request++;

		return true;
	}

	// $seconds is resolver time only, so page-render time can't trip the breaker.
	private static function record_dns_duration( float $seconds ): void {
		if ( $seconds >= self::DNS_SLOW_THRESHOLD ) {
			set_transient( self::DNS_BREAKER_TRANSIENT, 1, self::DNS_BREAKER_TTL );
		}
	}

	// Lazy path: call only when a block/429 is imminent. Returns a DnsVerifier
	// status, STATUS_NONE with no engine claim, STATUS_INELIGIBLE out of scope.
	public function resolve_enforcement_status( string $ip, string $user_agent, bool $require_eligibility ): string {
		$engine = SearchTrustCore::get_verified_search_engine_claim( $user_agent ) ?? '';
		if ( $engine === '' ) {
			return self::STATUS_NONE;
		}

		if ( $require_eligibility ) {
			$eligible = SearchTrustCore::is_verified_search_request_eligible(
				[
					'method' => (string) IpDetector::get_server_value( 'REQUEST_METHOD' ),
					'request_uri' => (string) IpDetector::get_server_value( 'REQUEST_URI' ),
				],
				$this->get_current_scope()
			);
			if ( ! $eligible ) {
				return self::STATUS_INELIGIBLE;
			}
		}

		// The verdict is pure IP+DNS, so a bootstrap check this request already ran applies regardless of scope.
		$bootstrap_context = SearchTrustContext::from_bootstrap_constant();
		if ( is_array( $bootstrap_context ) && ! empty( $bootstrap_context['checked'] ) ) {
			$status = (string) ( $bootstrap_context['status'] ?? '' );
			if ( $status !== '' ) {
				return $status;
			}
			if ( ! empty( $bootstrap_context['verified'] ) ) {
				return SearchTrustDnsVerifier::TRUST_VERIFIED;
			}
		}

		$cache_key = $engine . '|' . $ip;
		if ( ! isset( $this->enforcement_status_cache[ $cache_key ] ) ) {
			$this->enforcement_status_cache[ $cache_key ] = $this->classify_claimed_engine( $ip, $engine );
		}

		return $this->enforcement_status_cache[ $cache_key ];
	}

	/**
	 * @return array{protected_paths: array<int, string>, protected_prefixes: array<int, string>, scope_hash: string}
	 */
	private function get_current_scope(): array {
		if ( $this->current_scope === null ) {
			$this->current_scope = SearchTrustScopeBuilder::build_from_wordpress();
		}

		return $this->current_scope;
	}

	private function cache_verified_search_result( string $cache_key, bool $verified, int $ttl ): void {
		$now = time();
		$cache_value = SearchTrustCore::create_verified_search_cache_record(
			$verified,
			$now + max( 1, $ttl ),
			$now
		);

		if ( function_exists( 'wp_cache_set' ) ) {
			wp_cache_set( $cache_key, $cache_value, self::CACHE_GROUP, $ttl );
		}

		if ( function_exists( 'set_transient' ) ) {
			set_transient( $cache_key, $cache_value, $ttl );
		}
	}

	/**
	 * @param array{verified: bool, expires_at: int, updated_at: int, verified_at: int, last_seen_at: int} $record
	 */
	private function touch_verified_search_result( string $cache_key, array $record ): void {
		if ( function_exists( 'wp_cache_set' ) ) {
			wp_cache_set( $cache_key, $record, self::CACHE_GROUP, SearchTrustCore::get_verified_search_cache_ttl( true ) );
		}

		if ( function_exists( 'set_transient' ) ) {
			set_transient( $cache_key, $record, SearchTrustCore::get_verified_search_cache_ttl( true ) );
		}
	}

	/**
	 * @return array{verified: bool, expires_at: int, updated_at: int, verified_at: int, last_seen_at: int}|null
	 */
	private function get_cached_verified_search_result( string $cache_key ): ?array {
		if ( function_exists( 'wp_cache_get' ) ) {
			$found = false;
			$cached = wp_cache_get( $cache_key, self::CACHE_GROUP, false, $found );
			if ( $found ) {
				$record = SearchTrustCore::normalize_verified_search_cache_record( $cached );
				if ( $record !== null ) {
					return $record;
				}
			}
		}

		if ( function_exists( 'get_transient' ) ) {
			$cached = get_transient( $cache_key );
			$record = SearchTrustCore::normalize_verified_search_cache_record( $cached );
			if ( $record !== null ) {
				return $record;
			}
		}

		return null;
	}

	/**
	 * @return array{type: string, key: string, token: string}|null
	 */
	private function acquire_verified_search_lock( string $cache_key ): ?array {
		$lock_key = 'sitefort_vsearch_lock_' . md5( $cache_key );
		$token = $this->create_lock_token();
		$expires_at = time() + self::LOCK_TTL;
		$value = $token . '|' . $expires_at;
		$option_lock_supported = function_exists( 'add_option' ) && function_exists( 'get_option' ) && function_exists( 'delete_option' );

		if ( $option_lock_supported && add_option( $lock_key, $value, '', 'no' ) ) {
			return [
				'type' => 'option',
				'key' => $lock_key,
				'token' => $token,
			];
		}

		if ( $option_lock_supported ) {
			$current = (string) get_option( $lock_key, '' );
			$parts = explode( '|', $current, 2 );
			$current_expires_at = isset( $parts[1] ) ? (int) $parts[1] : 0;
			if ( $current !== '' && ( $current_expires_at <= 0 || $current_expires_at < time() ) ) {
				delete_option( $lock_key );
				if ( add_option( $lock_key, $value, '', 'no' ) ) {
					return [
						'type' => 'option',
						'key' => $lock_key,
						'token' => $token,
					];
				}
			}

			return null;
		}

		if ( function_exists( 'wp_cache_add' ) && wp_cache_add( $lock_key, $token, self::CACHE_GROUP, self::LOCK_TTL ) ) {
			return [
				'type' => 'cache',
				'key' => $lock_key,
				'token' => $token,
			];
		}

		return null;
	}

	private function release_verified_search_lock( $lock_token ): void {
		if ( ! is_array( $lock_token ) ) {
			return;
		}

		$type = (string) ( $lock_token['type'] ?? '' );
		$key = (string) ( $lock_token['key'] ?? '' );
		$token = (string) ( $lock_token['token'] ?? '' );
		if ( $key === '' || $token === '' ) {
			return;
		}

		if ( $type === 'option' && function_exists( 'get_option' ) && function_exists( 'delete_option' ) ) {
			$current = (string) get_option( $key, '' );
			if ( str_starts_with( $current, $token . '|' ) ) {
				delete_option( $key );
			}
			return;
		}

		if ( $type === 'cache' && function_exists( 'wp_cache_delete' ) ) {
			wp_cache_delete( $key, self::CACHE_GROUP );
		}
	}

	private function create_lock_token(): string {
		try {
			return bin2hex( random_bytes( 16 ) );
		} catch ( \Exception $e ) {
			return md5( uniqid( 'sitefort_vsearch_', true ) );
		}
	}
}
