<?php

declare(strict_types=1);

namespace App\Framework\Cache\Strategies;

use App\Framework\Cache\CacheKey;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Core\ValueObjects\Timestamp;

/**
 * Adaptive TTL Cache Strategy
 *
 * Dynamically adjusts TTL based on access patterns to optimize cache efficiency.
 * Frequently accessed items get longer TTL, rarely accessed items get shorter TTL.
 */
final class AdaptiveTtlCacheStrategy implements CacheStrategy
{
    private const int DEFAULT_LEARNING_WINDOW = 100;
    private const float DEFAULT_EXTENSION_FACTOR = 1.5;
    private const float DEFAULT_REDUCTION_FACTOR = 0.8;
    private const int ACCESS_THRESHOLD_HIGH = 10;
    private const int ACCESS_THRESHOLD_LOW = 2;

    /** @var array<string, AccessPattern> */
    private array $accessPatterns = [];

    /** @var array<string, AdaptiveTtlStats> */
    private array $itemStats = [];

    private readonly Duration $minTtl;

    private readonly Duration $maxTtl;

    public function __construct(
        private readonly bool $enabled = true,
        private readonly int $learningWindow = self::DEFAULT_LEARNING_WINDOW,
        private readonly float $extensionFactor = self::DEFAULT_EXTENSION_FACTOR,
        private readonly float $reductionFactor = self::DEFAULT_REDUCTION_FACTOR,
        ?Duration $minTtl = null,
        ?Duration $maxTtl = null
    ) {
        $this->minTtl = $minTtl ?? Duration::fromSeconds(60);
        $this->maxTtl = $maxTtl ?? Duration::fromSeconds(86400);
    }

    public function onCacheAccess(CacheKey $key, bool $isHit, ?Duration $retrievalTime = null): void
    {
        if (! $this->enabled) {
            return;
        }

        $keyString = $key->toString();

        // Record access pattern
        if (! isset($this->accessPatterns[$keyString])) {
            $this->accessPatterns[$keyString] = new AccessPattern($this->learningWindow);
        }
        $this->accessPatterns[$keyString]->recordAccess();

        // Record hit/miss statistics
        if (! isset($this->itemStats[$keyString])) {
            $this->itemStats[$keyString] = new AdaptiveTtlStats();
        }
        $this->itemStats[$keyString]->recordHitMiss($isHit);
    }

    public function onCacheSet(CacheKey $key, mixed $value, ?Duration $originalTtl): Duration
    {
        if (! $this->enabled) {
            return $originalTtl ?? Duration::fromHours(1);
        }

        return $this->calculateAdaptiveTtl($key, $originalTtl);
    }

    public function onCacheForget(CacheKey $key): void
    {
        if (! $this->enabled) {
            return;
        }

        $keyString = $key->toString();
        unset($this->accessPatterns[$keyString]);
        unset($this->itemStats[$keyString]);
    }

    public function getStats(): array
    {
        $stats = [
            'strategy' => 'AdaptiveTtlCacheStrategy',
            'enabled' => $this->enabled,
            'total_tracked_keys' => count($this->accessPatterns),
            'learning_window' => $this->learningWindow,
            'ttl_bounds' => [
                'min_seconds' => $this->minTtl->toSeconds(),
                'max_seconds' => $this->maxTtl->toSeconds(),
            ],
            'adaptation_factors' => [
                'extension_factor' => $this->extensionFactor,
                'reduction_factor' => $this->reductionFactor,
            ],
            'key_patterns' => [],
        ];

        // Include top accessed keys and their patterns
        $keysByAccess = [];
        foreach ($this->accessPatterns as $keyString => $pattern) {
            $keysByAccess[$keyString] = $pattern->getTotalAccesses();
        }
        arsort($keysByAccess);

        $topKeys = array_slice($keysByAccess, 0, 10, true);
        foreach ($topKeys as $keyString => $totalAccesses) {
            $pattern = $this->accessPatterns[$keyString];
            $itemStats = $this->itemStats[$keyString] ?? null;

            $stats['key_patterns'][$keyString] = [
                'total_accesses' => $totalAccesses,
                'recent_accesses' => $pattern->getRecentAccessCount(),
                'access_frequency' => round($pattern->getAccessFrequency(), 3),
                'hit_rate' => $itemStats ? round($itemStats->getHitRate(), 3) : null,
                'total_requests' => $itemStats?->getTotalRequests() ?? 0,
            ];
        }

        return $stats;
    }

    public function getName(): string
    {
        return 'adaptive_ttl';
    }

    public function clear(): void
    {
        $this->accessPatterns = [];
        $this->itemStats = [];
    }

    public function isEnabled(): bool
    {
        return $this->enabled;
    }

    /**
     * Calculate adaptive TTL based on access patterns and original TTL
     */
    private function calculateAdaptiveTtl(CacheKey $key, ?Duration $originalTtl): Duration
    {
        $keyString = $key->toString();
        $pattern = $this->accessPatterns[$keyString] ?? null;

        // Use original TTL as base, or default if not provided
        $baseTtl = $originalTtl ?? Duration::fromHours(1);
        $baseSeconds = $baseTtl->toSeconds();

        if (! $pattern) {
            // No access pattern yet, use original TTL
            return $this->enforceTtlBounds($baseTtl);
        }

        $recentAccesses = $pattern->getRecentAccessCount();
        $accessFrequency = $pattern->getAccessFrequency();
        $hitRate = $this->itemStats[$keyString]?->getHitRate() ?? 1.0;

        // Calculate adaptation factor based on access patterns
        $adaptationFactor = 1.0;

        // High access count and good hit rate = extend TTL
        if ($recentAccesses >= self::ACCESS_THRESHOLD_HIGH && $hitRate > 0.8) {
            $adaptationFactor = $this->extensionFactor;
        }
        // Low access count or poor hit rate = reduce TTL
        elseif ($recentAccesses <= self::ACCESS_THRESHOLD_LOW || $hitRate < 0.3) {
            $adaptationFactor = $this->reductionFactor;
        }
        // Moderate access = adjust based on frequency trend
        else {
            // If access frequency is increasing, slightly extend TTL
            // If decreasing, slightly reduce TTL
            $trendFactor = max(0.5, min(2.0, $accessFrequency));
            $adaptationFactor = 0.8 + ($trendFactor * 0.4); // Range: 0.8 to 1.2
        }

        $adaptedSeconds = (int) round($baseSeconds * $adaptationFactor);
        $adaptedTtl = Duration::fromSeconds($adaptedSeconds);

        return $this->enforceTtlBounds($adaptedTtl);
    }

    /**
     * Ensure TTL stays within configured bounds
     */
    private function enforceTtlBounds(Duration $ttl): Duration
    {
        $seconds = $ttl->toSeconds();
        $minSeconds = $this->minTtl->toSeconds();
        $maxSeconds = $this->maxTtl->toSeconds();

        $boundedSeconds = max($minSeconds, min($maxSeconds, $seconds));

        return Duration::fromSeconds($boundedSeconds);
    }
}

/**
 * Access pattern tracking for adaptive TTL calculation
 */
final class AccessPattern
{
    /** @var array<int, Timestamp> */
    private array $accessTimes = [];

    public function __construct(
        private readonly int $windowSize
    ) {
    }

    public function recordAccess(): void
    {
        $this->accessTimes[] = Timestamp::now();

        // Keep only recent accesses within the window
        if (count($this->accessTimes) > $this->windowSize) {
            array_shift($this->accessTimes);
        }
    }

    public function getRecentAccessCount(): int
    {
        return count($this->accessTimes);
    }

    public function getTotalAccesses(): int
    {
        return count($this->accessTimes);
    }

    /**
     * Calculate access frequency (accesses per hour)
     */
    public function getAccessFrequency(): float
    {
        if (count($this->accessTimes) < 2) {
            return 0.0;
        }

        $oldest = $this->accessTimes[0];
        $newest = end($this->accessTimes);
        $timeSpan = $newest->diff($oldest);

        if ($timeSpan->toSeconds() <= 0) {
            return 0.0;
        }

        $hours = $timeSpan->toSeconds() / 3600.0;

        return count($this->accessTimes) / $hours;
    }
}

/**
 * Statistics for adaptive TTL optimization
 */
final class AdaptiveTtlStats
{
    private int $hits = 0;

    private int $misses = 0;

    public function recordHitMiss(bool $isHit): void
    {
        if ($isHit) {
            $this->hits++;
        } else {
            $this->misses++;
        }
    }

    public function getHitRate(): float
    {
        $total = $this->hits + $this->misses;

        return $total > 0 ? ($this->hits / $total) : 0.0;
    }

    public function getTotalRequests(): int
    {
        return $this->hits + $this->misses;
    }

    public function getHits(): int
    {
        return $this->hits;
    }

    public function getMisses(): int
    {
        return $this->misses;
    }
}
