<?php

declare(strict_types=1);

namespace App\Framework\Cache\Warming\Strategies;

use App\Framework\Cache\Cache;
use App\Framework\Cache\CacheKey;
use App\Framework\Cache\CacheItem;
use App\Framework\Cache\Warming\ValueObjects\WarmupPriority;
use App\Framework\Core\ValueObjects\Duration;

/**
 * Predictive warming based on access patterns and historical data
 *
 * This strategy analyzes cache hit/miss patterns and proactively warms
 * cache entries that are likely to be accessed soon based on:
 * - Historical access patterns
 * - Time of day patterns
 * - Seasonal patterns
 * - User behavior patterns
 */
final readonly class PredictiveWarmingStrategy extends BaseWarmupStrategy
{
    private const ACCESS_PATTERN_CACHE_KEY = 'warmup_access_patterns';
    private const MIN_ACCESS_COUNT = 5; // Minimum accesses to consider for warmup
    private const PREDICTION_THRESHOLD = 0.7; // 70% probability threshold

    public function __construct(
        Cache $cache
    ) {
        parent::__construct($cache);
    }

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

    public function getPriority(): int
    {
        return WarmupPriority::BACKGROUND->value;
    }

    protected function getItemsToWarm(): array
    {
        $accessPatterns = $this->loadAccessPatterns();
        $predictions = $this->generatePredictions($accessPatterns);

        $items = [];
        foreach ($predictions as $prediction) {
            if ($prediction['probability'] >= self::PREDICTION_THRESHOLD) {
                $items[] = [
                    'key' => $prediction['cache_key'],
                    'loader' => $prediction['loader'],
                    'ttl' => $prediction['ttl'],
                    'probability' => $prediction['probability']
                ];
            }
        }

        return $items;
    }

    protected function warmItem(mixed $item): void
    {
        if (!is_array($item) || !isset($item['key'], $item['loader'], $item['ttl'])) {
            throw new \InvalidArgumentException('Invalid warmup item structure');
        }

        $key = CacheKey::fromString($item['key']);

        // Check if item is already in cache (don't override fresh data)
        if ($this->cache->has($key)) {
            return;
        }

        $data = $item['loader']();
        $ttl = $item['ttl'];

        $cacheItem = CacheItem::forSetting(
            key: $key,
            value: $data,
            ttl: $ttl
        );

        $this->cache->set($cacheItem);
    }

    private function loadAccessPatterns(): array
    {
        $key = CacheKey::fromString(self::ACCESS_PATTERN_CACHE_KEY);
        $result = $this->cache->get($key);

        if ($result->isHit()) {
            return $result->value;
        }

        return [];
    }

    private function generatePredictions(array $accessPatterns): array
    {
        $currentHour = (int) date('H');
        $currentDayOfWeek = (int) date('N'); // 1 (Monday) to 7 (Sunday)

        $predictions = [];

        foreach ($accessPatterns as $pattern) {
            $probability = $this->calculateProbability(
                pattern: $pattern,
                currentHour: $currentHour,
                currentDayOfWeek: $currentDayOfWeek
            );

            if ($probability >= self::PREDICTION_THRESHOLD) {
                $predictions[] = [
                    'cache_key' => $pattern['cache_key'],
                    'loader' => $pattern['loader'] ?? fn() => null,
                    'ttl' => $pattern['ttl'] ?? Duration::fromHours(1),
                    'probability' => $probability,
                    'reason' => $this->getPredictionReason($pattern, $currentHour, $currentDayOfWeek)
                ];
            }
        }

        // Sort by probability (highest first)
        usort($predictions, fn($a, $b) => $b['probability'] <=> $a['probability']);

        // Limit to top 50 predictions to avoid overwhelming the cache
        return array_slice($predictions, 0, 50);
    }

    private function calculateProbability(array $pattern, int $currentHour, int $currentDayOfWeek): float
    {
        $accessCount = $pattern['access_count'] ?? 0;

        if ($accessCount < self::MIN_ACCESS_COUNT) {
            return 0.0;
        }

        $hourlyPattern = $pattern['hourly_distribution'] ?? [];
        $dailyPattern = $pattern['daily_distribution'] ?? [];

        // Base probability from access frequency
        $baseProbability = min($accessCount / 100, 1.0);

        // Hour-based adjustment
        $hourlyWeight = $hourlyPattern[$currentHour] ?? 0.0;

        // Day-based adjustment
        $dailyWeight = $dailyPattern[$currentDayOfWeek] ?? 0.0;

        // Combined probability
        $probability = $baseProbability * 0.4 + $hourlyWeight * 0.4 + $dailyWeight * 0.2;

        return min($probability, 1.0);
    }

    private function getPredictionReason(array $pattern, int $currentHour, int $currentDayOfWeek): string
    {
        $reasons = [];

        if (($pattern['access_count'] ?? 0) >= 50) {
            $reasons[] = 'high access frequency';
        }

        $hourlyPattern = $pattern['hourly_distribution'] ?? [];
        if (($hourlyPattern[$currentHour] ?? 0.0) > 0.7) {
            $reasons[] = "typically accessed at hour {$currentHour}";
        }

        $dailyPattern = $pattern['daily_distribution'] ?? [];
        $dayName = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'][$currentDayOfWeek - 1];
        if (($dailyPattern[$currentDayOfWeek] ?? 0.0) > 0.7) {
            $reasons[] = "typically accessed on {$dayName}";
        }

        return !empty($reasons) ? implode(', ', $reasons) : 'access pattern detected';
    }

    protected function getMetadata(): array
    {
        $accessPatterns = $this->loadAccessPatterns();
        $predictions = $this->generatePredictions($accessPatterns);

        return [
            'total_patterns' => count($accessPatterns),
            'predictions_generated' => count($predictions),
            'top_probability' => !empty($predictions) ? round($predictions[0]['probability'], 2) : 0.0
        ];
    }

    public function shouldRun(): bool
    {
        // Only run predictive warming if we have enough historical data
        $accessPatterns = $this->loadAccessPatterns();
        return count($accessPatterns) >= 10;
    }

    public function getEstimatedDuration(): int
    {
        $predictions = $this->generatePredictions($this->loadAccessPatterns());
        return max(count($predictions) * 2, 10); // 2 seconds per prediction, min 10 seconds
    }
}
