<?php

namespace WPProofShield\Security;

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


use WP_Error;
use WPProofShield\Support\RequestInput;
use WPProofShield\Support\Settings;

final class RequestVerifier {


	private const HONEYPOT_FIELDS = array( 'wpsh_hp_a', 'wpsh_hp_b', 'wpsh_hp_c', 'wpsh_hp_d', 'wpsh_hp', 'address_line2', 'contact_fax', 'company_website', 'middle_name', 'secondary_phone', 'vat_number', 'department_code', 'office_extension' );

	/**
	 * @var array<string,scalar|null>
	 */
	private array $seededRequestValues = array();
	public function __construct(
		private Settings $settings,
		private ModeAdvisor $modeAdvisor,
		private TokenService $tokenService,
		private PowVerifier $powVerifier,
		private ReplayGuard $replayGuard,
		private RateLimiter $rateLimiter,
		private StatsStore $stats
	) {
	}

	/**
	 * @param array<string,scalar|null> $values
	 */
	public function seedRequestValues( array $values ): void {
		$this->seededRequestValues = $values;
	}

	public function clearSeedRequestValues(): void {
		$this->seededRequestValues = array();
	}

	public function verify( string $context, bool $requireToken ): bool|WP_Error {

		$ip                             = $this->remoteIp();
		$countPowFailuresTowardCooldown = $this->shouldCountPowFailuresTowardCooldown( $context );
		if ( $this->isTrustedIp( $ip, $context ) ) {
			$this->stats->record( $context, 'ok', 0, null );
			return true;
		}

		$token        = $this->requestText( 'wpsh_pow' );
		$counter_raw  = $this->requestText( 'wpsh_ctr' );
		$counter      = $counter_raw !== '' ? (int) $counter_raw : null;
		$providedHash = $this->requestText( 'wpsh_hash' );
		$hasFormProof = str_starts_with( $context, 'form:' ) && $token !== '' && $counter !== null;

		if ( $this->rateLimiter->inCooldown( $context, $ip ) && ! $hasFormProof ) {
			$this->stats->record( $context, 'wpsh_rate_limited', 0, null );
			return new WP_Error( 'wpsh_rate_limited', __( 'Too many requests. Please try again later.', 'proofshield' ) );
		}

		$failureCount = $this->rateLimiter->failureCount( $context, $ip );
		$thresholds   = $this->secureThresholdsForMode();

		if ( $token === '' || $counter === null ) {
			if ( $requireToken ) {
				if ( $this->settings->isEnabled( 'enabled_secure_plus' ) ) {
					$assessment = $this->assessSecureRisk( $context, $failureCount, null, false );
					if ( $assessment['score'] >= $thresholds['block'] ) {
						$this->applyTarpitDelay( $assessment['score'], $thresholds );
						if ( $countPowFailuresTowardCooldown ) {
							$this->rateLimiter->recordFailure( $context, $ip );
						}
						$this->stats->record( $context, 'wpsh_secure_block', 0, null );
						return new WP_Error( 'wpsh_secure_block', __( 'Request classified as high risk.', 'proofshield' ) );
					}

					if ( $assessment['score'] >= $thresholds['hard'] ) {
						$this->applyTarpitDelay( $assessment['score'], $thresholds );
						if ( $countPowFailuresTowardCooldown ) {
							$this->rateLimiter->recordFailure( $context, $ip );
						}
						$this->stats->record( $context, 'wpsh_hard_challenge', 0, null );
						return new WP_Error( 'wpsh_hard_challenge', __( 'Hard challenge required. Please submit again.', 'proofshield' ) );
					}
				}

				if ( $countPowFailuresTowardCooldown ) {
					$this->rateLimiter->recordFailure( $context, $ip );
				}
				$this->stats->record( $context, 'wpsh_pow_missing', 0, null );
				return new WP_Error( 'wpsh_pow_missing', __( 'ProofShield verification is missing.', 'proofshield' ) );
			}

			$this->rateLimiter->recordSuccess( $context, $ip );
			$this->stats->record( $context, 'ok', 0, null );
			return true;
		}

		$start  = microtime( true );
		$parsed = $this->tokenService->parseAndVerify( $token );
		if ( is_wp_error( $parsed ) ) {
			if ( $countPowFailuresTowardCooldown ) {
				$this->rateLimiter->recordFailure( $context, $ip );
			}
			$this->stats->record( $context, $parsed->get_error_code(), 0, null );
			return $parsed;
		}

		if ( (string) $parsed['ctx'] !== $context ) {
			if ( $countPowFailuresTowardCooldown ) {
				$this->rateLimiter->recordFailure( $context, $ip );
			}
			$this->stats->record( $context, 'wpsh_pow_invalid', (int) $parsed['diff'], null );
			return new WP_Error( 'wpsh_pow_invalid', __( 'ProofShield context mismatch.', 'proofshield' ) );
		}

		$honeypot = $this->honeypotValue( isset( $parsed['hpn'] ) ? (string) $parsed['hpn'] : '' );
		if ( $honeypot !== '' && $this->shouldEvaluateHoneypot( $context ) ) {
			$this->rateLimiter->forceCooldown( $context, $ip, 30 * MINUTE_IN_SECONDS, 'honeypot' );
			$this->stats->record( $context, 'wpsh_honeypot', 0, null );
			return new WP_Error( 'wpsh_honeypot', __( 'Bot pattern detected.', 'proofshield' ) );
		}
		$difficulty = (int) $parsed['diff'];
		if ( $this->settings->isEnabled( 'enabled_secure_plus' ) ) {
			$assessment         = $this->assessSecureRisk( $context, $failureCount, (int) ( $parsed['iat'] ?? 0 ), true );
			$requiredDifficulty = $this->requiredDifficultyForRisk( $assessment['score'], $thresholds );
			if ( $requiredDifficulty > 0 && $difficulty < $requiredDifficulty ) {
				$this->applyTarpitDelay( $assessment['score'], $thresholds );
				if ( $countPowFailuresTowardCooldown ) {
					$this->rateLimiter->recordFailure( $context, $ip );
				}
				$this->stats->record( $context, 'wpsh_hard_challenge', $difficulty, null );
				return new WP_Error( 'wpsh_hard_challenge', __( 'Hard challenge required. Please submit again.', 'proofshield' ) );
			}
		}
		$isValidPow = $this->powVerifier->verify( (string) $parsed['_encoded'], $counter, $difficulty, $providedHash );
		if ( ! $isValidPow ) {
			if ( $countPowFailuresTowardCooldown ) {
				$this->rateLimiter->recordFailure( $context, $ip );
			}
			$this->stats->record( $context, 'wpsh_pow_invalid', $difficulty, null );
			return new WP_Error( 'wpsh_pow_invalid', __( 'Invalid ProofShield hash.', 'proofshield' ) );
		}

		$ttl = max( 30, ( (int) $parsed['exp'] ) - time() );
		if ( ! $this->replayGuard->consumeNonce( (string) $parsed['nonce'], $ttl ) ) {
			if ( $countPowFailuresTowardCooldown ) {
				$this->rateLimiter->recordFailure( $context, $ip );
			}
			$this->stats->record( $context, 'wpsh_replay', $difficulty, null );
			return new WP_Error( 'wpsh_replay', __( 'ProofShield token was already used.', 'proofshield' ) );
		}

		$solveMs = (int) ( ( microtime( true ) - $start ) * 1000 );
		if ( $context !== 'login' ) {
			$this->rateLimiter->recordSuccess( $context, $ip );
		}
		$this->stats->record( $context, 'ok', $difficulty, $solveMs );

		return true;
	}

	public function adjustDifficultyForRisk( string $context, int $difficulty ): int {
		if ( ! $this->settings->isEnabled( 'enabled_secure_plus' ) ) {
			return $difficulty;
		}

		$ip         = $this->remoteIp();
		$failures   = $this->rateLimiter->failureCount( $context, $ip );
		$thresholds = $this->secureThresholdsForMode();
		$assessment = $this->assessSecureRisk( $context, $failures, null, false );
		if ( $assessment['score'] >= $thresholds['block'] ) {
			return min( 6, $difficulty + 2 );
		}
		if ( $assessment['score'] >= $thresholds['hard'] ) {
			return min( 6, $difficulty + 1 );
		}
		return min( 6, $difficulty );
	}

	private function remoteIp(): string {
		$candidate = isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( (string) $_SERVER['REMOTE_ADDR'] ) ) : '';
		if ( $this->isLocalHost() ) {
			$sim = isset( $_SERVER['HTTP_X_WPSH_SIM_IP'] ) ? trim( sanitize_text_field( wp_unslash( (string) $_SERVER['HTTP_X_WPSH_SIM_IP'] ) ) ) : '';
			if ( filter_var( $sim, FILTER_VALIDATE_IP ) ) {
				$candidate = $sim;
			}
		}
		return filter_var( $candidate, FILTER_VALIDATE_IP ) ? $candidate : '0.0.0.0';
	}

	private function isTrustedIp( string $ip, string $context ): bool {
		if ( $this->isLocalDevelopmentBypass( $ip, $context ) ) {
			return true;
		}

		$raw = trim( (string) $this->settings->get( 'trusted_ips', '' ) );
		if ( $raw === '' || ! filter_var( $ip, FILTER_VALIDATE_IP ) ) {
			return false;
		}

		$rules = array_filter( array_map( 'trim', preg_split( '/\R+/', $raw ) ?: array() ) );
		foreach ( $rules as $rule ) {
			if ( $rule === $ip ) {
				return true;
			}

			if ( $this->ipMatchesCidr( $ip, $rule ) ) {
				return true;
			}
		}

		return false;
	}

	private function ipMatchesCidr( string $ip, string $cidr ): bool {
		if ( ! str_contains( $cidr, '/' ) ) {
			return false;
		}

		[$subnet, $maskRaw] = array_map( 'trim', explode( '/', $cidr, 2 ) );
		if ( ! filter_var( $subnet, FILTER_VALIDATE_IP ) ) {
			return false;
		}
		if ( ! ctype_digit( $maskRaw ) ) {
			return false;
		}

		$mask = (int) $maskRaw;

		$ipLong     = ip2long( $ip );
		$subnetLong = ip2long( $subnet );
		if ( $ipLong === false || $subnetLong === false ) {
			return false;
		}

		if ( $mask < 0 || $mask > 32 ) {
			return false;
		}

		$bitmask = $mask === 0 ? 0 : ( -1 << ( 32 - $mask ) );
		return ( ( $ipLong & $bitmask ) === ( $subnetLong & $bitmask ) );
	}

	private function isLocalDevelopmentBypass( string $ip, string $context ): bool {
		if ( $context !== 'login' ) {
			return false;
		}

		if ( ! $this->isLocalHost() ) {
			return false;
		}

		if ( $ip === '127.0.0.1' || $ip === '::1' ) {
			return true;
		}

		if ( ! filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 ) ) {
			return false;
		}

		return str_starts_with( $ip, '10.' )
			|| str_starts_with( $ip, '192.168.' )
			|| preg_match( '/^172\.(1[6-9]|2[0-9]|3[0-1])\./', $ip ) === 1;
	}

	private function isLocalHost(): bool {
		$host = (string) wp_parse_url( home_url( '/' ), PHP_URL_HOST );
		return in_array( $host, array( 'localhost', '127.0.0.1', '::1' ), true );
	}

	private function honeypotValue( string $expectedField = '' ): string {

		if ( $expectedField === '' ) {
			return '';
		}

		$expectedField = sanitize_key( $expectedField );
		if ( ! in_array( $expectedField, self::HONEYPOT_FIELDS, true ) ) {
			return '';
		}
		return $this->requestText( $expectedField );
	}
	private function shouldEvaluateHoneypot( string $context ): bool {
		$method = isset( $_SERVER['REQUEST_METHOD'] ) ? strtoupper( sanitize_text_field( wp_unslash( (string) $_SERVER['REQUEST_METHOD'] ) ) ) : 'GET';
		if ( $method !== 'POST' ) {
			return false;
		}

		if ( $context === 'login' ) {
			$user = $this->requestText( 'log' );
			$pass = $this->requestText( 'pwd' );
			return $user !== '' && $pass !== '';
		}

		return true;
	}

	private function assessSecureRisk( string $context, int $failureCount, ?int $tokenIat, bool $hasToken ): array {
		$methods = $this->selectedSecureMethods();
		$score   = 0;

		if ( in_array( 'failure_burst', $methods, true ) ) {
			$score += min( 40, max( 0, $failureCount ) * 10 );
		}

		if ( in_array( 'headless_signature', $methods, true ) && $this->looksLikeHeadlessUa() ) {
			$score += 35;
		}

		if ( in_array( 'header_consistency', $methods, true ) ) {
			$score += $this->headerAnomalyScore( $context );
		}

		if ( $hasToken && in_array( 'fast_submit', $methods, true ) && $tokenIat !== null && $tokenIat > 0 ) {
			$age = max( 0, time() - $tokenIat );
			if ( $age <= 1 ) {
				$score += 20;
			} elseif ( $age <= 2 ) {
				$score += 12;
			}
		}

		if ( ! $hasToken && ( $context === 'login' || str_starts_with( $context, 'form:' ) ) ) {
			$score += 10;
		}

		return array( 'score' => $score );
	}

	private function shouldCountPowFailuresTowardCooldown( string $context ): bool {
		// Login cooldown should be driven by repeated credential failures in LoginGuard,
		// not by PoW misses/retries/page refreshes.
		if ( $context === 'login' ) {
			return false;
		}

		return true;
	}

	private function requiredDifficultyForRisk( int $score, array $thresholds ): int {
		if ( $score >= $thresholds['block'] ) {
			return 6;
		}
		if ( $score >= $thresholds['hard'] ) {
			return 5;
		}
		return 0;
	}

	private function secureThresholdsForMode(): array {
		return match ( $this->modeAdvisor->effectiveMode() ) {
			'relaxed' => array(
				'hard'     => 65,
				'block'    => 85,
				'delay_ms' => 1200,
			),
			'strict' => array(
				'hard'     => 45,
				'block'    => 65,
				'delay_ms' => 3500,
			),
			default => array(
				'hard'     => 55,
				'block'    => 75,
				'delay_ms' => 2200,
			),
		};
	}

	private function selectedSecureMethods(): array {
		$raw     = $this->settings->get( 'secure_plus_methods', array() );
		$methods = is_array( $raw ) ? array_values( array_map( 'strval', $raw ) ) : array();
		if ( $methods === array() ) {
			return array( 'header_consistency', 'headless_signature', 'failure_burst', 'fast_submit', 'tarpit_timeout' );
		}
		return $methods;
	}

	private function applyTarpitDelay( int $score, array $thresholds ): void {
		if ( ! in_array( 'tarpit_timeout', $this->selectedSecureMethods(), true ) ) {
			return;
		}

		if ( $score < $thresholds['hard'] ) {
			return;
		}

		$base    = (int) ( $thresholds['delay_ms'] ?? 1500 );
		$factor  = $score >= $thresholds['block'] ? 2 : 1;
		$delayMs = min( 12000, $base * $factor );
		usleep( $delayMs * 1000 );
	}

	private function looksLikeHeadlessUa(): bool {
		$ua = isset( $_SERVER['HTTP_USER_AGENT'] ) ? strtolower( sanitize_text_field( wp_unslash( (string) $_SERVER['HTTP_USER_AGENT'] ) ) ) : '';
		if ( $ua === '' ) {
			return true;
		}

		$needles = array( 'headless', 'selenium', 'playwright', 'puppeteer', 'phantomjs', 'python-requests', 'curl/', 'wget/' );
		foreach ( $needles as $needle ) {
			if ( str_contains( $ua, $needle ) ) {
				return true;
			}
		}

		return false;
	}

	private function headerAnomalyScore( string $context ): int {
		$score  = 0;
		$method = isset( $_SERVER['REQUEST_METHOD'] ) ? strtoupper( sanitize_text_field( wp_unslash( (string) $_SERVER['REQUEST_METHOD'] ) ) ) : 'GET';
		if ( $method === 'POST' && ( $context === 'login' || $context === 'comment' || str_starts_with( $context, 'form:' ) ) ) {
			$origin  = isset( $_SERVER['HTTP_ORIGIN'] ) ? esc_url_raw( wp_unslash( (string) $_SERVER['HTTP_ORIGIN'] ) ) : '';
			$referer = isset( $_SERVER['HTTP_REFERER'] ) ? esc_url_raw( wp_unslash( (string) $_SERVER['HTTP_REFERER'] ) ) : '';
			if ( $origin === '' && $referer === '' ) {
				$score += 14;
			}
		}

		$secFetch = isset( $_SERVER['HTTP_SEC_FETCH_SITE'] ) ? sanitize_text_field( wp_unslash( (string) $_SERVER['HTTP_SEC_FETCH_SITE'] ) ) : '';
		if ( $secFetch === '' ) {
			$score += 8;
		}

		$acceptLang = isset( $_SERVER['HTTP_ACCEPT_LANGUAGE'] ) ? sanitize_text_field( wp_unslash( (string) $_SERVER['HTTP_ACCEPT_LANGUAGE'] ) ) : '';
		if ( $acceptLang === '' ) {
			$score += 6;
		}

		return $score;
	}

	private function requestText( string $key ): string {
		if ( array_key_exists( $key, $this->seededRequestValues ) ) {
			$value = $this->seededRequestValues[ $key ];
			return is_scalar( $value ) ? trim( sanitize_text_field( (string) $value ) ) : '';
		}

		return RequestInput::postText( $key );
	}
}
