<?php

declare(strict_types=1);

namespace App\Framework\Waf\BotProtection;

use App\Framework\Cache\Cache;
use App\Framework\Cache\CacheKey;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\DateTime\Clock;
use App\Framework\HttpClient\HttpClient;
use App\Framework\Logging\Logger;
use App\Framework\Waf\BotProtection\ValueObjects\CaptchaChallenge;
use App\Framework\Waf\BotProtection\ValueObjects\CaptchaResult;

/**
 * CAPTCHA validation service supporting multiple providers
 */
final class CaptchaValidator
{
    private const RECAPTCHA_VERIFY_URL = 'https://www.google.com/recaptcha/api/siteverify';
    private const HCAPTCHA_VERIFY_URL = 'https://hcaptcha.com/siteverify';

    public function __construct(
        private readonly HttpClient $httpClient,
        private readonly Clock $clock,
        private readonly Logger $logger,
        private readonly Cache $cache,
        private readonly bool $enabled = true,
        private readonly string $provider = 'recaptcha', // 'recaptcha' or 'hcaptcha'
        private readonly ?string $secretKey = null,
        private readonly ?string $siteKey = null,
        private readonly float $minimumScore = 0.5,
        private readonly int $challengeTimeout = 300, // 5 minutes
        private array $activeChallenges = []
    ) {
    }

    /**
     * Generate CAPTCHA challenge for suspicious requests
     */
    public function generateChallenge(string $sessionId, float $riskScore, array $context = []): CaptchaChallenge
    {
        if (! $this->enabled || ! $this->secretKey || ! $this->siteKey) {
            return CaptchaChallenge::disabled();
        }

        try {
            $challengeId = $this->generateChallengeId();
            $expiresAt = $this->clock->time()->addSeconds($this->challengeTimeout);

            $challenge = new CaptchaChallenge(
                challengeId: $challengeId,
                sessionId: $sessionId,
                provider: $this->provider,
                siteKey: $this->siteKey,
                riskScore: $riskScore,
                createdAt: $this->clock->time(),
                expiresAt: $expiresAt,
                isRequired: $riskScore >= 70.0,
                context: $context
            );

            // Store active challenge
            $this->activeChallenges[$challengeId] = [
                'challenge' => $challenge,
                'created_at' => $this->clock->time()->toUnixTimestamp(),
            ];

            // Cache challenge for persistence
            $this->cache->set(
                CacheKey::fromString("captcha_challenge:{$challengeId}"),
                $challenge->toArray(),
                Duration::fromSeconds($this->challengeTimeout)
            );

            $this->logger->info('CAPTCHA challenge generated', [
                'challenge_id' => $challengeId,
                'session_id' => $sessionId,
                'provider' => $this->provider,
                'risk_score' => $riskScore,
                'required' => $challenge->isRequired,
            ]);

            return $challenge;

        } catch (\Throwable $e) {
            $this->logger->error('Failed to generate CAPTCHA challenge', [
                'error' => $e->getMessage(),
                'session_id' => $sessionId,
                'risk_score' => $riskScore,
            ]);

            return CaptchaChallenge::disabled();
        }
    }

    /**
     * Validate CAPTCHA response
     */
    public function validateResponse(string $challengeId, string $response, string $clientIp): CaptchaResult
    {
        if (! $this->enabled || ! $this->secretKey) {
            return CaptchaResult::disabled();
        }

        try {
            // Get challenge from cache or memory
            $challenge = $this->getChallenge($challengeId);
            if (! $challenge) {
                return CaptchaResult::invalid('Challenge not found or expired');
            }

            // Check if challenge has expired
            if ($challenge->expiresAt->isBefore($this->clock->time())) {
                $this->removeChallenge($challengeId);

                return CaptchaResult::invalid('Challenge expired');
            }

            // Validate with provider
            $providerResult = $this->validateWithProvider($response, $clientIp);

            // Create result
            $result = new CaptchaResult(
                isValid: $providerResult['success'],
                score: $providerResult['score'] ?? null,
                action: $providerResult['action'] ?? null,
                hostname: $providerResult['hostname'] ?? null,
                challengeId: $challengeId,
                provider: $this->provider,
                validatedAt: $this->clock->time(),
                errorCodes: $providerResult['error_codes'] ?? [],
                metadata: [
                    'client_ip' => $clientIp,
                    'response_time' => $providerResult['response_time'] ?? null,
                    'challenge_context' => $challenge->context,
                ]
            );

            // Remove challenge after validation
            $this->removeChallenge($challengeId);

            $this->logger->info('CAPTCHA validation completed', [
                'challenge_id' => $challengeId,
                'provider' => $this->provider,
                'valid' => $result->isValid,
                'score' => $result->score,
                'client_ip' => $clientIp,
            ]);

            return $result;

        } catch (\Throwable $e) {
            $this->logger->error('CAPTCHA validation failed', [
                'challenge_id' => $challengeId,
                'error' => $e->getMessage(),
                'client_ip' => $clientIp,
            ]);

            return CaptchaResult::invalid('Validation error: ' . $e->getMessage());
        }
    }

    /**
     * Validate response with external provider
     */
    private function validateWithProvider(string $response, string $clientIp): array
    {
        $startTime = microtime(true);

        $url = match($this->provider) {
            'hcaptcha' => self::HCAPTCHA_VERIFY_URL,
            default => self::RECAPTCHA_VERIFY_URL
        };

        $postData = [
            'secret' => $this->secretKey,
            'response' => $response,
            'remoteip' => $clientIp,
        ];

        try {
            $httpResponse = $this->httpClient->post($url, [
                'form_params' => $postData,
                'timeout' => 10,
                'headers' => [
                    'Content-Type' => 'application/x-www-form-urlencoded',
                ],
            ]);

            $responseTime = microtime(true) - $startTime;
            $data = json_decode($httpResponse->getBody(), true);

            if (json_last_error() !== JSON_ERROR_NONE) {
                throw new \RuntimeException('Invalid JSON response from CAPTCHA provider');
            }

            // Handle provider-specific response format
            return $this->normalizeProviderResponse($data, $responseTime);

        } catch (\Throwable $e) {
            $this->logger->warning('CAPTCHA provider request failed', [
                'provider' => $this->provider,
                'error' => $e->getMessage(),
                'response_time' => microtime(true) - $startTime,
            ]);

            throw $e;
        }
    }

    /**
     * Normalize provider response to common format
     */
    private function normalizeProviderResponse(array $data, float $responseTime): array
    {
        $normalized = [
            'success' => $data['success'] ?? false,
            'response_time' => $responseTime,
            'error_codes' => $data['error-codes'] ?? [],
        ];

        // Provider-specific handling
        switch ($this->provider) {
            case 'recaptcha':
                $normalized['score'] = $data['score'] ?? null;
                $normalized['action'] = $data['action'] ?? null;
                $normalized['hostname'] = $data['hostname'] ?? null;

                // For reCAPTCHA v3, check minimum score
                if ($normalized['score'] !== null && $normalized['score'] < $this->minimumScore) {
                    $normalized['success'] = false;
                    $normalized['error_codes'][] = 'score-threshold-not-met';
                }

                break;

            case 'hcaptcha':
                // hCaptcha doesn't provide score, so we treat success as binary
                $normalized['score'] = $normalized['success'] ? 1.0 : 0.0;

                break;
        }

        return $normalized;
    }

    /**
     * Get challenge from cache or memory
     */
    private function getChallenge(string $challengeId): ?CaptchaChallenge
    {
        // Try memory first
        if (isset($this->activeChallenges[$challengeId])) {
            return $this->activeChallenges[$challengeId]['challenge'];
        }

        // Try cache
        $cached = $this->cache->get(CacheKey::fromString("captcha_challenge:{$challengeId}"));
        if ($cached) {
            return CaptchaChallenge::fromArray($cached);
        }

        return null;
    }

    /**
     * Remove challenge from memory and cache
     */
    private function removeChallenge(string $challengeId): void
    {
        unset($this->activeChallenges[$challengeId]);
        $this->cache->delete(CacheKey::fromString("captcha_challenge:{$challengeId}"));
    }

    /**
     * Generate unique challenge ID
     */
    private function generateChallengeId(): string
    {
        return hash('sha256', uniqid('captcha_', true) . microtime(true) . random_bytes(16));
    }

    /**
     * Check if CAPTCHA is required for given risk score
     */
    public function isRequired(float $riskScore): bool
    {
        return $this->enabled && $riskScore >= 50.0;
    }

    /**
     * Check if CAPTCHA should be mandatory (no bypass)
     */
    public function isMandatory(float $riskScore): bool
    {
        return $this->enabled && $riskScore >= 80.0;
    }

    /**
     * Clean up expired challenges
     */
    public function cleanupExpiredChallenges(): int
    {
        $currentTime = $this->clock->time()->toUnixTimestamp();
        $removed = 0;

        foreach ($this->activeChallenges as $challengeId => $data) {
            if ($currentTime - $data['created_at'] > $this->challengeTimeout) {
                $this->removeChallenge($challengeId);
                $removed++;
            }
        }

        return $removed;
    }

    /**
     * Get validator statistics
     */
    public function getStatistics(): array
    {
        return [
            'enabled' => $this->enabled,
            'provider' => $this->provider,
            'minimum_score' => $this->minimumScore,
            'challenge_timeout_seconds' => $this->challengeTimeout,
            'active_challenges' => count($this->activeChallenges),
            'has_credentials' => ! empty($this->secretKey) && ! empty($this->siteKey),
        ];
    }

    /**
     * Check if validator is properly configured
     */
    public function isConfigured(): bool
    {
        return $this->enabled && ! empty($this->secretKey) && ! empty($this->siteKey);
    }

    /**
     * Get supported providers
     */
    public static function getSupportedProviders(): array
    {
        return [
            'recaptcha' => [
                'name' => 'Google reCAPTCHA',
                'versions' => ['v2', 'v3'],
                'features' => ['score_based', 'action_based', 'hostname_validation'],
            ],
            'hcaptcha' => [
                'name' => 'hCaptcha',
                'versions' => ['v1'],
                'features' => ['privacy_focused', 'gdpr_compliant'],
            ],
        ];
    }
}
