<?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;

/**
 * Heat Map Cache Strategy
 *
 * Tracks cache usage patterns to identify hot/cold keys and performance bottlenecks.
 * Provides optimization insights and recommendations.
 */
final class HeatMapCacheStrategy implements CacheStrategy
{
    private const int DEFAULT_MAX_TRACKED_KEYS = 10000;
    private const int DEFAULT_HOT_THRESHOLD = 20;  // accesses per hour
    private const int DEFAULT_COLD_THRESHOLD = 1;  // accesses per hour
    private const int DEFAULT_ANALYSIS_WINDOW_HOURS = 2;

    /** @var array<string, HeatMapEntry> */
    private array $heatMap = [];

    /** @var array<string, WriteOperation> */
    private array $writeOperations = [];

    public function __construct(
        private readonly bool $enabled = true,
        private readonly int $maxTrackedKeys = self::DEFAULT_MAX_TRACKED_KEYS,
        private readonly int $hotThreshold = self::DEFAULT_HOT_THRESHOLD,
        private readonly int $coldThreshold = self::DEFAULT_COLD_THRESHOLD,
        private readonly int $analysisWindowHours = self::DEFAULT_ANALYSIS_WINDOW_HOURS
    ) {
    }

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

        $keyString = $key->toString();

        // Prevent memory overflow by limiting tracked keys
        if (! isset($this->heatMap[$keyString]) && count($this->heatMap) >= $this->maxTrackedKeys) {
            $this->evictOldestEntry();
        }

        if (! isset($this->heatMap[$keyString])) {
            $this->heatMap[$keyString] = new HeatMapEntry($key);
        }

        $this->heatMap[$keyString]->recordAccess($isHit, $retrievalTime);
    }

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

        $keyString = $key->toString();
        $valueSize = $this->calculateValueSize($value);
        $setTime = Duration::fromMilliseconds(1); // Minimal set time for now

        $this->writeOperations[$keyString] = new WriteOperation(
            $key,
            $valueSize,
            $setTime,
            Timestamp::now()
        );

        return $originalTtl ?? Duration::fromHours(1);
    }

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

        $keyString = $key->toString();
        unset($this->heatMap[$keyString]);
        unset($this->writeOperations[$keyString]);
    }

    public function getStats(): array
    {
        $analysis = $this->getHeatMapAnalysis();
        $bottlenecks = $this->getPerformanceBottlenecks();

        return [
            'strategy' => 'HeatMapCacheStrategy',
            'enabled' => $this->enabled,
            'total_tracked_keys' => count($this->heatMap),
            'max_tracked_keys' => $this->maxTrackedKeys,
            'thresholds' => [
                'hot_threshold' => $this->hotThreshold,
                'cold_threshold' => $this->coldThreshold,
                'analysis_window_hours' => $this->analysisWindowHours,
            ],
            'analysis' => $analysis,
            'performance_bottlenecks' => $bottlenecks,
        ];
    }

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

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

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

    /**
     * Get comprehensive heat map analysis
     */
    public function getHeatMapAnalysis(): array
    {
        $hotKeys = [];
        $coldKeys = [];
        $performanceInsights = [];

        $cutoffTime = Timestamp::now()->subtract(Duration::fromHours($this->analysisWindowHours));

        foreach ($this->heatMap as $keyString => $entry) {
            $recentAccesses = $entry->getRecentAccesses($cutoffTime);
            $accessesPerHour = $this->calculateAccessesPerHour($recentAccesses);

            if ($accessesPerHour >= $this->hotThreshold) {
                $hotKeys[] = [
                    'key' => $keyString,
                    'accesses_per_hour' => round($accessesPerHour, 2),
                    'hit_rate' => round($entry->getHitRate(), 3),
                    'avg_retrieval_time_ms' => round($entry->getAverageRetrievalTime() * 1000, 2),
                    'total_accesses' => count($recentAccesses),
                ];
            } elseif ($accessesPerHour <= $this->coldThreshold) {
                $coldKeys[] = [
                    'key' => $keyString,
                    'accesses_per_hour' => round($accessesPerHour, 2),
                    'hit_rate' => round($entry->getHitRate(), 3),
                    'last_access' => $entry->getLastAccessTime()?->format('Y-m-d H:i:s'),
                ];
            }

            // Performance insights
            if ($entry->getAverageRetrievalTime() > 0.1) { // >100ms
                $performanceInsights[] = [
                    'type' => 'slow_retrieval',
                    'key' => $keyString,
                    'avg_time_ms' => round($entry->getAverageRetrievalTime() * 1000, 2),
                    'recommendation' => 'Consider optimizing data source or adding caching layer',
                ];
            }

            if ($entry->getHitRate() < 0.5) { // <50% hit rate
                $performanceInsights[] = [
                    'type' => 'low_hit_rate',
                    'key' => $keyString,
                    'hit_rate' => round($entry->getHitRate(), 3),
                    'recommendation' => 'Review TTL settings or access patterns',
                ];
            }
        }

        // Sort by priority
        usort($hotKeys, fn ($a, $b) => $b['accesses_per_hour'] <=> $a['accesses_per_hour']);
        usort($coldKeys, fn ($a, $b) => $a['accesses_per_hour'] <=> $b['accesses_per_hour']);

        return [
            'total_tracked_keys' => count($this->heatMap),
            'hot_keys' => array_slice($hotKeys, 0, 10),
            'cold_keys' => array_slice($coldKeys, 0, 10),
            'performance_insights' => $performanceInsights,
            'analysis_window_hours' => $this->analysisWindowHours,
        ];
    }

    /**
     * Get performance bottlenecks with impact scoring
     */
    public function getPerformanceBottlenecks(): array
    {
        $bottlenecks = [];

        foreach ($this->heatMap as $keyString => $entry) {
            $avgRetrievalTime = $entry->getAverageRetrievalTime();
            $hitRate = $entry->getHitRate();
            $accessCount = $entry->getTotalAccesses();

            // Calculate impact score (higher = more critical)
            $impactScore = 0;

            // Slow retrieval impact
            if ($avgRetrievalTime > 0.1) {
                $impactScore += ($avgRetrievalTime * 10) * $accessCount;
            }

            // Low hit rate impact
            if ($hitRate < 0.7) {
                $impactScore += (1 - $hitRate) * $accessCount * 5;
            }

            // High miss rate with frequent access
            if ($hitRate < 0.5 && $accessCount > 50) {
                $impactScore += 100;
            }

            if ($impactScore > 10) {
                $bottlenecks[] = [
                    'key' => $keyString,
                    'impact_score' => $impactScore,
                    'type' => $this->classifyBottleneck($avgRetrievalTime, $hitRate),
                    'avg_retrieval_time_ms' => round($avgRetrievalTime * 1000, 2),
                    'hit_rate' => round($hitRate, 3),
                    'access_count' => $accessCount,
                    'recommendation' => $this->getBottleneckRecommendation($avgRetrievalTime, $hitRate),
                ];
            }
        }

        // Sort by impact score (highest first)
        usort($bottlenecks, fn ($a, $b) => $b['impact_score'] <=> $a['impact_score']);

        return array_slice($bottlenecks, 0, 20); // Top 20 bottlenecks
    }

    /**
     * Get hot keys (most frequently accessed)
     */
    public function getHotKeys(int $limit = 10): array
    {
        $hotKeys = [];
        $cutoffTime = Timestamp::now()->subtract(Duration::fromHours($this->analysisWindowHours));

        foreach ($this->heatMap as $keyString => $entry) {
            $recentAccesses = $entry->getRecentAccesses($cutoffTime);
            $accessesPerHour = $this->calculateAccessesPerHour($recentAccesses);

            if ($accessesPerHour >= $this->hotThreshold) {
                $hotKeys[$keyString] = $accessesPerHour;
            }
        }

        arsort($hotKeys);

        return array_slice($hotKeys, 0, $limit, true);
    }

    private function evictOldestEntry(): void
    {
        if (empty($this->heatMap)) {
            return;
        }

        $oldestKey = null;
        $oldestTime = null;

        foreach ($this->heatMap as $keyString => $entry) {
            $lastAccess = $entry->getLastAccessTime();
            if ($oldestTime === null || ($lastAccess && $lastAccess->isBefore($oldestTime))) {
                $oldestTime = $lastAccess;
                $oldestKey = $keyString;
            }
        }

        if ($oldestKey !== null) {
            unset($this->heatMap[$oldestKey]);
        }
    }

    private function calculateValueSize(mixed $value): int
    {
        try {
            return strlen(serialize($value));
        } catch (\Throwable) {
            return 0;
        }
    }

    private function calculateAccessesPerHour(array $accesses): float
    {
        if (empty($accesses)) {
            return 0.0;
        }

        $hours = $this->analysisWindowHours;

        return count($accesses) / $hours;
    }

    private function classifyBottleneck(float $retrievalTime, float $hitRate): string
    {
        if ($retrievalTime > 0.2) {
            return 'slow_retrieval';
        }

        if ($hitRate < 0.3) {
            return 'very_low_hit_rate';
        }

        if ($hitRate < 0.7) {
            return 'low_hit_rate';
        }

        return 'performance_issue';
    }

    private function getBottleneckRecommendation(float $retrievalTime, float $hitRate): string
    {
        if ($retrievalTime > 0.2) {
            return 'Optimize data source or add intermediate caching layer';
        }

        if ($hitRate < 0.3) {
            return 'Review cache key patterns and TTL settings';
        }

        if ($hitRate < 0.7) {
            return 'Consider increasing TTL or improving data freshness strategy';
        }

        return 'Monitor and analyze access patterns';
    }
}

/**
 * Heat map entry for tracking cache key usage
 */
final class HeatMapEntry
{
    /** @var array<array{timestamp: Timestamp, is_hit: bool, retrieval_time: ?float}> */
    private array $accesses = [];

    private int $totalHits = 0;

    private int $totalMisses = 0;

    private float $totalRetrievalTime = 0.0;

    private int $retrievalTimeCount = 0;

    public function __construct(
        private readonly CacheKey $key
    ) {
    }

    public function recordAccess(bool $isHit, ?Duration $retrievalTime = null): void
    {
        $this->accesses[] = [
            'timestamp' => Timestamp::now(),
            'is_hit' => $isHit,
            'retrieval_time' => $retrievalTime?->toSeconds(),
        ];

        if ($isHit) {
            $this->totalHits++;
        } else {
            $this->totalMisses++;
        }

        if ($retrievalTime !== null) {
            $this->totalRetrievalTime += $retrievalTime->toSeconds();
            $this->retrievalTimeCount++;
        }

        // Keep only recent data to prevent memory bloat
        $cutoff = Timestamp::now()->subtract(Duration::fromHours(48)); // Keep 48 hours
        $this->accesses = array_filter(
            $this->accesses,
            fn ($access) => $access['timestamp']->isAfter($cutoff)
        );
    }

    public function getRecentAccesses(Timestamp $since): array
    {
        return array_filter(
            $this->accesses,
            fn ($access) => $access['timestamp']->isAfter($since)
        );
    }

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

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

    public function getAverageRetrievalTime(): float
    {
        return $this->retrievalTimeCount > 0 ? ($this->totalRetrievalTime / $this->retrievalTimeCount) : 0.0;
    }

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

    public function getLastAccessTime(): ?Timestamp
    {
        if (empty($this->accesses)) {
            return null;
        }

        return end($this->accesses)['timestamp'];
    }

    public function getKey(): CacheKey
    {
        return $this->key;
    }
}

/**
 * Write operation tracking
 */
final readonly class WriteOperation
{
    public function __construct(
        public CacheKey $key,
        public int $valueSize,
        public Duration $writeTime,
        public Timestamp $timestamp
    ) {
    }
}
