<?php

declare(strict_types=1);

namespace SiteFort\Modules\Firewall\SearchTrust;

final class SearchTrustCore {
	private const VERIFIED_SEARCH_ENGINES = [
		// No .googleusercontent.com: any GCP tenant can get forward-confirmable rDNS under it.
		'googlebot' => [
			'ua_tokens' => [ 'googlebot' ],
			'dns_suffixes' => [ '.googlebot.com', '.google.com' ],
		],
		'bingbot' => [
			'ua_tokens' => [ 'bingbot' ],
			'dns_suffixes' => [ '.search.msn.com' ],
		],
	];

	private const VERIFIED_SEARCH_CACHE_TTL_POSITIVE = 604800;
	private const VERIFIED_SEARCH_CACHE_TTL_NEGATIVE = 900;
	private const VERIFIED_SEARCH_CACHE_TTL_INDETERMINATE = 300;
	private const VERIFIED_SEARCH_CACHE_REFRESH_INTERVAL = 86400;
	private const VERIFIED_SEARCH_CACHE_HARD_REVALIDATE_AFTER = 604800;
	private const VERIFIED_SEARCH_CACHE_STALE_GRACE = 86400;

	public static function get_verified_search_engine_claim( string $user_agent ): ?string {
		$user_agent = strtolower( trim( $user_agent ) );
		if ( $user_agent === '' ) {
			return null;
		}

		foreach ( self::VERIFIED_SEARCH_ENGINES as $engine => $config ) {
			foreach ( (array) ( $config['ua_tokens'] ?? [] ) as $token ) {
				$token = strtolower( trim( (string) $token ) );
				if ( $token !== '' && str_contains( $user_agent, $token ) ) {
					return $engine;
				}
			}
		}

		return null;
	}

	/**
	 * @return array<int, string>
	 */
	public static function get_verified_search_dns_suffixes( string $engine ): array {
		$engine = strtolower( trim( $engine ) );
		if ( $engine === '' || ! isset( self::VERIFIED_SEARCH_ENGINES[ $engine ] ) ) {
			return [];
		}

		$suffixes = [];
		foreach ( (array) self::VERIFIED_SEARCH_ENGINES[ $engine ]['dns_suffixes'] as $suffix ) {
			$suffix = strtolower( trim( (string) $suffix ) );
			if ( $suffix !== '' ) {
				$suffixes[ $suffix ] = true;
			}
		}

		return array_keys( $suffixes );
	}

	public static function hostname_matches_verified_search_engine( string $hostname, string $engine ): bool {
		$hostname = rtrim( strtolower( trim( $hostname ) ), '.' );
		if ( $hostname === '' ) {
			return false;
		}

		foreach ( self::get_verified_search_dns_suffixes( $engine ) as $suffix ) {
			$suffix = rtrim( $suffix, '.' );
			if ( $suffix !== '' && ( $hostname === ltrim( $suffix, '.' ) || str_ends_with( $hostname, $suffix ) ) ) {
				return true;
			}
		}

		return false;
	}

	public static function get_verified_search_cache_ttl( bool $verified ): int {
		return $verified
			? self::VERIFIED_SEARCH_CACHE_TTL_POSITIVE
			: self::VERIFIED_SEARCH_CACHE_TTL_NEGATIVE;
	}

	public static function get_verified_search_cache_ttl_indeterminate(): int {
		return self::VERIFIED_SEARCH_CACHE_TTL_INDETERMINATE;
	}

	public static function get_verified_search_cache_refresh_interval(): int {
		return self::VERIFIED_SEARCH_CACHE_REFRESH_INTERVAL;
	}

	public static function get_verified_search_cache_hard_revalidate_after(): int {
		return self::VERIFIED_SEARCH_CACHE_HARD_REVALIDATE_AFTER;
	}

	public static function get_verified_search_cache_stale_grace(): int {
		return self::VERIFIED_SEARCH_CACHE_STALE_GRACE;
	}

	/**
	 * @return array{verified: bool, expires_at: int, updated_at: int, verified_at: int, last_seen_at: int}
	 */
	public static function create_verified_search_cache_record(
		bool $verified,
		int $expires_at = 0,
		int $updated_at = 0,
		int $verified_at = 0,
		int $last_seen_at = 0
	): array {
		$now = time();
		$ttl = self::get_verified_search_cache_ttl( $verified );

		if ( $updated_at <= 0 ) {
			$updated_at = $now;
		}

		if ( $expires_at <= 0 ) {
			$expires_at = $now + $ttl;
		}

		if ( $verified ) {
			if ( $verified_at <= 0 ) {
				$verified_at = $now;
			}
			if ( $last_seen_at <= 0 ) {
				$last_seen_at = $now;
			}
		} else {
			$verified_at = 0;
			$last_seen_at = $last_seen_at > 0 ? $last_seen_at : $now;
		}

		return [
			'verified' => $verified,
			'expires_at' => $expires_at,
			'updated_at' => $updated_at,
			'verified_at' => $verified_at,
			'last_seen_at' => $last_seen_at,
		];
	}

	/**
	 * Normalize scalar cache values and structured records.
	 *
	 * @param mixed $cached
	 * @return array{verified: bool, expires_at: int, updated_at: int, verified_at: int, last_seen_at: int}|null
	 */
	public static function normalize_verified_search_cache_record( $cached ): ?array {
		if ( is_bool( $cached ) ) {
			return self::create_verified_search_cache_record( $cached );
		}

		if ( is_string( $cached ) ) {
			if ( $cached === 'yes' ) {
				return self::create_verified_search_cache_record( true );
			}

			if ( $cached === 'no' ) {
				return self::create_verified_search_cache_record( false );
			}

			return null;
		}

		if ( ! is_array( $cached ) || ! array_key_exists( 'verified', $cached ) ) {
			return null;
		}

		$verified = $cached['verified'];
		if ( is_string( $verified ) ) {
			$verified = strtolower( trim( $verified ) );
			if ( $verified === 'yes' || $verified === 'true' || $verified === '1' ) {
				$verified = true;
			} elseif ( $verified === 'no' || $verified === 'false' || $verified === '0' ) {
				$verified = false;
			}
		}

		if ( ! is_bool( $verified ) ) {
			return null;
		}

		$now = time();
		$ttl = self::get_verified_search_cache_ttl( $verified );
		$updated_at = max( 0, (int) ( $cached['updated_at'] ?? 0 ) );
		if ( $updated_at > $now ) {
			$updated_at = $now;
		}

		$expires_at = max( 0, (int) ( $cached['expires_at'] ?? 0 ) );
		if ( $expires_at <= 0 ) {
			$expires_at = $updated_at > 0 ? $updated_at + $ttl : $now + $ttl;
		}
		$expires_at = min( $expires_at, $now + $ttl );
		if ( $expires_at <= $now ) {
			return null;
		}

		$last_seen_at = max( 0, (int) ( $cached['last_seen_at'] ?? 0 ) );
		if ( $last_seen_at <= 0 ) {
			$last_seen_at = $updated_at;
		} elseif ( $last_seen_at > $now ) {
			$last_seen_at = $now;
		}

		$verified_at = max( 0, (int) ( $cached['verified_at'] ?? 0 ) );
		if ( $verified && $verified_at <= 0 ) {
			if ( $updated_at > 0 ) {
				$verified_at = $updated_at;
			} elseif ( $expires_at > 0 ) {
				$verified_at = max( 1, $expires_at - self::get_verified_search_cache_ttl( true ) );
			} else {
				$verified_at = $now;
			}
		} elseif ( $verified_at > $now ) {
			$verified_at = $now;
		}

		return [
			'verified' => $verified,
			'expires_at' => $expires_at,
			'updated_at' => $updated_at,
			'verified_at' => $verified ? $verified_at : 0,
			'last_seen_at' => $last_seen_at > 0 ? $last_seen_at : $now,
		];
	}

	/**
	 * @param array{verified: bool, expires_at: int, updated_at: int, verified_at: int, last_seen_at: int} $record
	 */
	public static function should_refresh_verified_search_cache_record( array $record ): bool {
		if ( empty( $record['verified'] ) ) {
			return false;
		}

		$now = time();
		$updated_at = max( 0, (int) ( $record['updated_at'] ?? 0 ) );
		$refresh_interval = max( 1, min(
			self::get_verified_search_cache_refresh_interval(),
			self::get_verified_search_cache_ttl( true )
		) );

		if ( $updated_at > 0 ) {
			return ( $now - $updated_at ) >= $refresh_interval;
		}

		$expires_at = max( 0, (int) ( $record['expires_at'] ?? 0 ) );
		if ( $expires_at <= 0 ) {
			return true;
		}

		return ( self::get_verified_search_cache_ttl( true ) - ( $expires_at - $now ) ) >= $refresh_interval;
	}

	/**
	 * @param array{verified: bool, expires_at: int, updated_at: int, verified_at: int, last_seen_at: int} $record
	 */
	public static function is_verified_search_cache_record_within_hard_age( array $record ): bool {
		if ( empty( $record['verified'] ) ) {
			return false;
		}

		$verified_at = max( 0, (int) ( $record['verified_at'] ?? 0 ) );
		if ( $verified_at <= 0 ) {
			return false;
		}

		return ( time() - $verified_at ) < self::get_verified_search_cache_hard_revalidate_after();
	}

	/**
	 * @param array{verified: bool, expires_at: int, updated_at: int, verified_at: int, last_seen_at: int} $record
	 */
	public static function is_verified_search_cache_record_within_stale_grace( array $record ): bool {
		if ( empty( $record['verified'] ) ) {
			return false;
		}

		$verified_at = max( 0, (int) ( $record['verified_at'] ?? 0 ) );
		if ( $verified_at <= 0 ) {
			return false;
		}

		$age = time() - $verified_at;
		return $age >= self::get_verified_search_cache_hard_revalidate_after()
			&& $age < ( self::get_verified_search_cache_hard_revalidate_after() + self::get_verified_search_cache_stale_grace() );
	}

	/**
	 * @param array{verified: bool, expires_at: int, updated_at: int, verified_at: int, last_seen_at: int} $record
	 * @return array{verified: bool, expires_at: int, updated_at: int, verified_at: int, last_seen_at: int}
	 */
	public static function refresh_verified_search_cache_record( array $record ): array {
		$now = time();

		return [
			'verified' => true,
			'expires_at' => $now + self::get_verified_search_cache_ttl( true ),
			'updated_at' => $now,
			'verified_at' => max( 1, (int) ( $record['verified_at'] ?? 0 ) ),
			'last_seen_at' => $now,
		];
	}

	public static function build_verified_search_cache_key( string $engine, string $ip ): string {
		return 'sitefort_vsearch_v4_' . md5( strtolower( trim( $engine ) ) . '|' . trim( $ip ) );
	}

	public static function get_verified_search_cache_shard( string $cache_key, int $shard_count = 32 ): string {
		$cache_key = trim( $cache_key );
		$shard_count = max( 1, $shard_count );

		if ( $cache_key === '' ) {
			return '00';
		}

		$hash = hash( 'crc32b', $cache_key );
		$shard = (int) ( hexdec( substr( $hash, -4 ) ) % $shard_count );

		return str_pad( (string) $shard, 2, '0', STR_PAD_LEFT );
	}

	public static function normalize_ip( string $ip ): string {
		$ip = trim( $ip );
		if ( $ip === '' ) {
			return '';
		}

		$packed = @inet_pton( $ip );
		if ( $packed !== false ) {
			if ( strlen( $packed ) === 16 ) {
				if ( substr( $packed, 0, 12 ) === "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff" ) {
					$ipv4 = @inet_ntop( substr( $packed, 12 ) );
					if ( is_string( $ipv4 ) && $ipv4 !== '' ) {
						return $ipv4;
					}
				}
			}

			$normalized = @inet_ntop( $packed );
			if ( is_string( $normalized ) && $normalized !== '' ) {
				return strtolower( $normalized );
			}
		}

		return $ip;
	}

	public static function should_resolve_verified_search_exception(
		bool $rate_limit_enabled,
		bool $country_check_active,
		bool $community_check_active,
		bool $verify_search_engine_claim = false
	): bool {
		return $rate_limit_enabled || $country_check_active || $community_check_active || $verify_search_engine_claim;
	}

	/**
	 * @param array<string, mixed> $request
	 * @param array<string, mixed> $scope
	 */
	public static function is_verified_search_request_eligible( array $request, array $scope = [] ): bool {
		$method = strtoupper( trim( (string) ( $request['method'] ?? 'GET' ) ) );
		if ( ! in_array( $method, [ 'GET', 'HEAD' ], true ) ) {
			return false;
		}

		$request_uri = (string) ( $request['request_uri'] ?? '' );
		$path = $request_uri !== ''
			? self::request_uri_component( $request_uri, 'path' )
			: (string) ( $request['path'] ?? '' );
		$query = $request_uri !== ''
			? self::request_uri_component( $request_uri, 'query' )
			: (string) ( $request['query'] ?? '' );

		$normalized_path = self::normalize_absolute_request_path( $path );
		if (
			self::request_matches_scope_path( $normalized_path, (array) ( $scope['protected_paths'] ?? [] ) )
			|| self::request_matches_scope_prefix( $normalized_path, (array) ( $scope['protected_prefixes'] ?? [] ) )
		) {
			return false;
		}

		return ! self::has_query_parameter( $query, 'rest_route' );
	}

	public static function request_uri_component( string $request_uri, string $component ): string {
		$uri = explode( '#', $request_uri, 2 )[0];
		$pos = strpos( $uri, '?' );

		if ( $component === 'query' ) {
			return $pos === false ? '' : (string) substr( $uri, $pos + 1 );
		}

		$path = $pos === false ? $uri : (string) substr( $uri, 0, $pos );
		return $path !== '' ? $path : '/';
	}

	/**
	 * @param array<int, string> $paths
	 * @return array<int, string>
	 */
	public static function normalize_request_scope_paths( array $paths ): array {
		$normalized = [];
		foreach ( $paths as $path ) {
			$path = self::normalize_absolute_request_path( (string) $path );
			if ( $path !== '' ) {
				$normalized[ $path ] = true;
			}
		}

		return array_keys( $normalized );
	}

	/**
	 * @param array<int, string> $prefixes
	 * @return array<int, string>
	 */
	public static function normalize_request_scope_prefixes( array $prefixes ): array {
		$normalized = [];
		foreach ( $prefixes as $prefix ) {
			$prefix = self::normalize_absolute_request_path( (string) $prefix );
			if ( $prefix !== '' && $prefix !== '/' ) {
				$normalized[ $prefix ] = true;
			}
		}

		return array_keys( $normalized );
	}

	/**
	 * @param array<string, mixed> $scope
	 */
	public static function build_scope_hash( array $scope ): string {
		$normalized_scope = [
			'protected_paths' => self::normalize_request_scope_paths( (array) ( $scope['protected_paths'] ?? [] ) ),
			'protected_prefixes' => self::normalize_request_scope_prefixes( (array) ( $scope['protected_prefixes'] ?? [] ) ),
		];

		$encoded = json_encode( $normalized_scope, JSON_UNESCAPED_SLASHES );
		if ( ! is_string( $encoded ) ) {
			$encoded = serialize( $normalized_scope );
		}

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

	private static function normalize_absolute_request_path( string $path ): string {
		$path = rawurldecode( trim( $path ) );
		if ( $path === '' ) {
			return '/';
		}

		$path = str_replace( '\\', '/', $path );
		$path = preg_replace( '#/+#', '/', $path ) ?? $path;
		$path = '/' . ltrim( $path, '/' );

		return $path === '/' ? '/' : rtrim( $path, '/' );
	}

	/**
	 * @param array<int, string> $paths
	 */
	private static function request_matches_scope_path( string $path, array $paths ): bool {
		if ( $path === '' ) {
			return false;
		}

		$path = self::normalize_absolute_request_path( $path );
		$normalized_paths = self::normalize_request_scope_paths( $paths );

		return in_array( $path, $normalized_paths, true );
	}

	/**
	 * @param array<int, string> $prefixes
	 */
	private static function request_matches_scope_prefix( string $path, array $prefixes ): bool {
		if ( $path === '' ) {
			return false;
		}

		$path = self::normalize_absolute_request_path( $path );
		foreach ( self::normalize_request_scope_prefixes( $prefixes ) as $prefix ) {
			if ( $path === $prefix || str_starts_with( $path, $prefix . '/' ) ) {
				return true;
			}
		}

		return false;
	}

	private static function has_query_parameter( string $query, string $parameter ): bool {
		$query = ltrim( trim( $query ), '?' );
		$parameter = trim( $parameter );
		if ( $query === '' || $parameter === '' ) {
			return false;
		}

		$params = [];
		parse_str( $query, $params );

		return array_key_exists( $parameter, $params );
	}
}
