<?php

declare(strict_types=1);

namespace App\Framework\RateLimit;

use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Core\ValueObjects\Timestamp;
use App\Framework\RateLimit\TimeProvider\SystemTimeProvider;
use App\Framework\RateLimit\TimeProvider\TimeProviderInterface;
use App\Framework\SlidingWindow\Aggregator\RateLimitResult as SlidingWindowRateLimitResult;
use App\Framework\SlidingWindow\SlidingWindow;
use App\Framework\SlidingWindow\SlidingWindowFactory;

/**
 * Modern RateLimiter using SlidingWindow module with advanced analytics
 */
final readonly class SlidingWindowRateLimiter
{
    private SlidingWindowFactory $windowFactory;

    public function __construct(
        SlidingWindowFactory $windowFactory,
        private TimeProviderInterface $timeProvider = new SystemTimeProvider(),
        private bool $persistent = true
    ) {
        $this->windowFactory = $windowFactory;
    }

    /**
     * Check rate limit with basic counting
     */
    public function checkLimit(string $key, int $limit, int $windowSeconds): RateLimitResult
    {
        $window = $this->windowFactory->createCountingWindow(
            identifier: "rate_limit:{$key}",
            windowSize: Duration::fromSeconds($windowSeconds),
            persistent: $this->persistent
        );

        $now = Timestamp::fromFloat($this->timeProvider->getCurrentTime());

        // Record this request
        $window->record(1, $now);

        // Get current count
        $stats = $window->getStats($now);
        $currentCount = $stats->totalCount;

        if ($currentCount > $limit) {
            // Calculate retry after based on oldest request in window
            $values = $window->getValues($now);
            if (! empty($values)) {
                $retryAfter = $windowSeconds;
            } else {
                $retryAfter = 1;
            }

            return RateLimitResult::exceeded($limit, $currentCount, $retryAfter);
        }

        return RateLimitResult::allowed($limit, $currentCount);
    }

    /**
     * Check rate limit with advanced threat analysis
     */
    public function checkLimitWithAnalysis(
        string $key,
        int $limit,
        int $windowSeconds,
        array $requestContext = []
    ): RateLimitResult {
        $window = $this->windowFactory->createRateLimitWindow(
            identifier: "rate_limit_advanced:{$key}",
            windowSize: Duration::fromSeconds($windowSeconds),
            persistent: $this->persistent
        );

        $now = Timestamp::fromFloat($this->timeProvider->getCurrentTime());

        // Record this request
        $window->record(1, $now);

        // Get advanced analytics
        $stats = $window->getStats($now);
        $aggregatedData = $stats->aggregatedData;

        $currentCount = $stats->totalCount;
        $isAllowed = $currentCount <= $limit;

        if ($aggregatedData['rate_limit'] instanceof SlidingWindowRateLimitResult) {
            $rateLimitResult = $aggregatedData['rate_limit'];

            // Enhanced analysis with context
            $analysisData = $this->enhanceAnalysisWithContext(
                $rateLimitResult->threatAnalysis,
                $requestContext
            );

            $retryAfter = $isAllowed ? 0 : $this->calculateRetryAfter($windowSeconds, $rateLimitResult);

            return RateLimitResult::withThreatAnalysis(
                $isAllowed,
                $limit,
                $currentCount,
                array_merge($analysisData, [
                    'burst_analysis' => $rateLimitResult->getBurstAnalysis(),
                    'sliding_window_stats' => [
                        'window_start' => $stats->windowStart->toFloat(),
                        'window_end' => $stats->windowEnd->toFloat(),
                        'window_size_seconds' => $stats->windowSize->toSeconds(),
                    ],
                ]),
                $retryAfter
            );
        }

        $retryAfter = $isAllowed ? 0 : $windowSeconds;

        return RateLimitResult::withThreatAnalysis($isAllowed, $limit, $currentCount, [], $retryAfter);
    }

    /**
     * Token bucket implementation using SlidingWindow for tracking
     */
    public function checkTokenBucket(string $key, int $capacity, int $refillRate, int $tokens = 1): RateLimitResult
    {
        // For token bucket, we'll use a different approach with the existing TokenBucket class
        // but track requests using SlidingWindow for analytics
        $window = $this->windowFactory->createRateLimitWindow(
            identifier: "token_bucket_tracking:{$key}",
            windowSize: Duration::fromMinutes(5), // Track for analytics
            persistent: $this->persistent
        );

        $now = Timestamp::fromFloat($this->timeProvider->getCurrentTime());

        // Record token consumption request
        $window->record($tokens, $now);

        // Use traditional token bucket logic but enhance with sliding window analytics
        $bucket = $this->getOrCreateTokenBucket($key, $capacity, $now->toFloat());

        // Refill tokens
        $timePassed = $now->toFloat() - $bucket->lastRefill;
        $tokensToAdd = min($capacity - $bucket->tokens, intval($timePassed * $refillRate));

        $bucket = $bucket->refill($tokensToAdd, intval($now->toFloat()));

        if ($bucket->canConsume($tokens)) {
            $bucket = $bucket->consume($tokens);
            $this->saveTokenBucket($key, $bucket);

            return RateLimitResult::allowed($capacity, $bucket->tokens);
        }

        $this->saveTokenBucket($key, $bucket);
        $retryAfter = intval(($tokens - $bucket->tokens) / $refillRate);

        return RateLimitResult::exceeded($capacity, $bucket->tokens, $retryAfter);
    }

    /**
     * Reset rate limiting for a key
     */
    public function reset(string $key): void
    {
        // Reset both types of windows
        $countingWindow = $this->windowFactory->createCountingWindow(
            identifier: "rate_limit:{$key}",
            windowSize: Duration::fromSeconds(60), // Doesn't matter for reset
            persistent: $this->persistent
        );

        $advancedWindow = $this->windowFactory->createRateLimitWindow(
            identifier: "rate_limit_advanced:{$key}",
            windowSize: Duration::fromSeconds(60), // Doesn't matter for reset
            persistent: $this->persistent
        );

        $countingWindow->clear();
        $advancedWindow->clear();
    }

    /**
     * Get usage statistics for a key
     */
    public function getUsage(string $key, int $windowSeconds): int
    {
        $window = $this->windowFactory->createCountingWindow(
            identifier: "rate_limit:{$key}",
            windowSize: Duration::fromSeconds($windowSeconds),
            persistent: $this->persistent
        );

        $now = Timestamp::fromFloat($this->timeProvider->getCurrentTime());
        $stats = $window->getStats($now);

        return $stats->totalCount;
    }

    /**
     * Get advanced analytics for a key
     */
    public function getAdvancedAnalytics(string $key, int $windowSeconds): array
    {
        $window = $this->windowFactory->createRateLimitWindow(
            identifier: "rate_limit_advanced:{$key}",
            windowSize: Duration::fromSeconds($windowSeconds),
            persistent: $this->persistent
        );

        $now = Timestamp::fromFloat($this->timeProvider->getCurrentTime());
        $stats = $window->getStats($now);

        if (isset($stats->aggregatedData['rate_limit']) &&
            $stats->aggregatedData['rate_limit'] instanceof SlidingWindowRateLimitResult) {
            return $stats->aggregatedData['rate_limit']->toArray();
        }

        return ['count' => $stats->totalCount, 'analytics' => []];
    }

    /**
     * Enhance analysis with request context
     */
    private function enhanceAnalysisWithContext(array $threatAnalysis, array $requestContext): array
    {
        $enhanced = $threatAnalysis;

        // Add request context analysis
        if (! empty($requestContext)) {
            $enhanced['request_context'] = [
                'client_ip' => $requestContext['client_ip'] ?? null,
                'user_agent' => $requestContext['user_agent'] ?? null,
                'request_path' => $requestContext['request_path'] ?? null,
            ];

            // Analyze user agent for suspicious patterns
            if (isset($requestContext['user_agent'])) {
                $enhanced['user_agent_analysis'] = $this->analyzeUserAgent($requestContext['user_agent']);
            }
        }

        return $enhanced;
    }

    /**
     * Analyze user agent for suspicious patterns
     */
    private function analyzeUserAgent(string $userAgent): array
    {
        $suspicious = false;
        $patterns = [];

        $suspiciousPatterns = [
            'bot' => '/bot|crawler|spider|scraper/i',
            'scanner' => '/scan|nikto|nmap|sqlmap|dirb/i',
            'automation' => '/curl|wget|python|java|go-http/i',
        ];

        foreach ($suspiciousPatterns as $type => $pattern) {
            if (preg_match($pattern, $userAgent)) {
                $suspicious = true;
                $patterns[] = $type;
            }
        }

        return [
            'suspicious' => $suspicious,
            'patterns' => $patterns,
            'user_agent' => $userAgent,
        ];
    }

    /**
     * Calculate retry after based on sliding window data
     */
    private function calculateRetryAfter(int $windowSeconds, SlidingWindowRateLimitResult $result): int
    {
        if (empty($result->timestamps)) {
            return $windowSeconds;
        }

        // Find oldest timestamp and calculate when it will expire
        $timestamps = $result->getTimestampsAsFloats();
        $oldestTimestamp = min($timestamps);
        $now = $this->timeProvider->getCurrentTime();

        $retryAfter = intval($oldestTimestamp + $windowSeconds - $now);

        return max(1, $retryAfter);
    }

    /**
     * Get or create token bucket (simplified for demo - in real implementation use storage)
     */
    private function getOrCreateTokenBucket(string $key, int $capacity, float $now): TokenBucket
    {
        // This would typically use the storage interface
        // For now, create a new bucket (not persistent)
        return new TokenBucket($capacity, $capacity, intval($now));
    }

    /**
     * Save token bucket (simplified for demo - in real implementation use storage)
     */
    private function saveTokenBucket(string $key, TokenBucket $bucket): void
    {
        // This would typically use the storage interface
        // For now, this is a no-op (not persistent)
    }
}
