<?php

declare(strict_types=1);

namespace App\Framework\Waf\BotProtection\Detectors;

use App\Framework\DateTime\Clock;
use App\Framework\Logging\Logger;
use App\Framework\Waf\Analysis\ValueObjects\RequestAnalysisData;
use App\Framework\Waf\BotProtection\ValueObjects\BrowserFingerprint;

/**
 * Browser fingerprinting detector for bot identification
 */
final class FingerprintDetector
{
    public function __construct(
        private readonly Clock $clock,
        private readonly Logger $logger,
        private readonly bool $enabled = true,
        private readonly int $cacheTimeout = 3600, // 1 hour
        private array $fingerprintCache = [],
        private array $knownBotFingerprints = []
    ) {
    }

    /**
     * Analyze browser fingerprint for bot indicators
     */
    public function analyzeFingerprint(RequestAnalysisData $requestData, array $context): BrowserFingerprint
    {
        if (! $this->enabled) {
            return $this->createEmptyFingerprint();
        }

        try {
            // Extract fingerprint data from request
            $fingerprintData = $this->extractFingerprintData($requestData, $context);

            // Check cache first
            $cacheKey = $this->generateCacheKey($fingerprintData);
            if (isset($this->fingerprintCache[$cacheKey])) {
                $cached = $this->fingerprintCache[$cacheKey];
                if ($this->clock->time()->toUnixTimestamp() - $cached['timestamp'] < $this->cacheTimeout) {
                    return $cached['fingerprint'];
                }
            }

            // Create fingerprint
            $fingerprint = BrowserFingerprint::fromData($fingerprintData, $this->clock->time());

            // Cache the result
            $this->fingerprintCache[$cacheKey] = [
                'fingerprint' => $fingerprint,
                'timestamp' => $this->clock->time()->toUnixTimestamp(),
            ];

            // Limit cache size
            if (count($this->fingerprintCache) > 10000) {
                $this->cleanupCache();
            }

            return $fingerprint;

        } catch (\Throwable $e) {
            $this->logger->error('Fingerprint analysis failed', [
                'error' => $e->getMessage(),
                'request_id' => $context['request_id'] ?? null,
            ]);

            return $this->createEmptyFingerprint();
        }
    }

    /**
     * Extract fingerprint data from request
     */
    private function extractFingerprintData(RequestAnalysisData $requestData, array $context): array
    {
        $data = [
            'user_agent' => $requestData->userAgent?->toString(),
            'accept_language' => $requestData->headers['accept-language'] ?? null,
            'accept_encoding' => $requestData->headers['accept-encoding'] ?? null,
        ];

        // Extract JavaScript-provided fingerprint data if available
        if (isset($context['fingerprint_data'])) {
            $jsData = $context['fingerprint_data'];

            // Canvas fingerprint
            if (isset($jsData['canvas'])) {
                $data['canvas'] = [
                    'data' => $jsData['canvas']['data'] ?? null,
                    'width' => $jsData['canvas']['width'] ?? null,
                    'height' => $jsData['canvas']['height'] ?? null,
                ];
            }

            // WebGL fingerprint
            if (isset($jsData['webgl'])) {
                $data['webgl'] = [
                    'vendor' => $jsData['webgl']['vendor'] ?? null,
                    'renderer' => $jsData['webgl']['renderer'] ?? null,
                    'version' => $jsData['webgl']['version'] ?? null,
                    'extensions' => $jsData['webgl']['extensions'] ?? [],
                ];
            }

            // Audio fingerprint
            if (isset($jsData['audio'])) {
                $data['audio'] = [
                    'context_hash' => $jsData['audio']['context_hash'] ?? null,
                    'oscillator_hash' => $jsData['audio']['oscillator_hash'] ?? null,
                ];
            }

            // Font detection
            if (isset($jsData['fonts'])) {
                $data['fonts'] = $jsData['fonts'];
            }

            // Screen information
            if (isset($jsData['screen'])) {
                $data['screen'] = [
                    $jsData['screen']['width'] ?? 0,
                    $jsData['screen']['height'] ?? 0,
                    $jsData['screen']['color_depth'] ?? 0,
                ];
            }

            // Timezone information
            if (isset($jsData['timezone'])) {
                $data['timezone'] = [
                    'offset' => $jsData['timezone']['offset'] ?? null,
                    'name' => $jsData['timezone']['name'] ?? null,
                ];
            }

            // Plugin information
            if (isset($jsData['plugins'])) {
                $data['plugins'] = $jsData['plugins'];
            }

            // Touch support
            $data['touch_support'] = $jsData['touch_support'] ?? false;

            // Color depth
            $data['color_depth'] = $jsData['color_depth'] ?? null;

            // Pixel ratio
            $data['pixel_ratio'] = $jsData['pixel_ratio'] ?? null;
        }

        return $data;
    }

    /**
     * Create empty fingerprint for disabled/error cases
     */
    private function createEmptyFingerprint(): BrowserFingerprint
    {
        return BrowserFingerprint::fromData([], $this->clock->time());
    }

    /**
     * Generate cache key for fingerprint data
     */
    private function generateCacheKey(array $data): string
    {
        return hash('sha256', json_encode($data));
    }

    /**
     * Clean up old cache entries
     */
    private function cleanupCache(): void
    {
        $currentTime = $this->clock->time()->toUnixTimestamp();

        foreach ($this->fingerprintCache as $key => $entry) {
            if ($currentTime - $entry['timestamp'] > $this->cacheTimeout) {
                unset($this->fingerprintCache[$key]);
            }
        }

        // If still too large, remove oldest entries
        if (count($this->fingerprintCache) > 5000) {
            $sorted = $this->fingerprintCache;
            uasort($sorted, fn ($a, $b) => $a['timestamp'] <=> $b['timestamp']);

            $toRemove = array_slice(array_keys($sorted), 0, 2500, true);
            foreach ($toRemove as $key) {
                unset($this->fingerprintCache[$key]);
            }
        }
    }

    /**
     * Add known bot fingerprint
     */
    public function addKnownBotFingerprint(string $fingerprintHash, array $metadata = []): void
    {
        $this->knownBotFingerprints[$fingerprintHash] = [
            'added_at' => $this->clock->time()->toUnixTimestamp(),
            'metadata' => $metadata,
        ];
    }

    /**
     * Check if fingerprint is known bot
     */
    public function isKnownBot(string $fingerprintHash): bool
    {
        return isset($this->knownBotFingerprints[$fingerprintHash]);
    }

    /**
     * Get detector statistics
     */
    public function getStatistics(): array
    {
        return [
            'enabled' => $this->enabled,
            'cache_size' => count($this->fingerprintCache),
            'cache_timeout_seconds' => $this->cacheTimeout,
            'known_bot_fingerprints' => count($this->knownBotFingerprints),
        ];
    }

    /**
     * Check if detector is enabled
     */
    public function isEnabled(): bool
    {
        return $this->enabled;
    }
}
