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

/**
 * Predictive Cache Strategy
 *
 * Uses machine learning patterns to predict cache access and proactively warm cache.
 * Analyzes time-based patterns, frequency patterns, and dependency relationships.
 */
final class PredictiveCacheStrategy implements CacheStrategy
{
    private const int DEFAULT_PREDICTION_WINDOW_HOURS = 24;
    private const int MIN_PATTERN_OCCURRENCES = 3;
    private const float DEFAULT_CONFIDENCE_THRESHOLD = 0.7;
    private const int MAX_CONCURRENT_WARMING = 5;

    /** @var array<string, PredictionPattern> */
    private array $patterns = [];

    /** @var array<string, WarmingJob> */
    private array $activeWarmingJobs = [];

    /** @var array<string, WarmingResult> */
    private array $warmingHistory = [];

    /** @var array<string, callable> */
    private array $warmingCallbacks = [];

    public function __construct(
        private readonly bool $enabled = true,
        private readonly int $predictionWindowHours = self::DEFAULT_PREDICTION_WINDOW_HOURS,
        private readonly float $confidenceThreshold = self::DEFAULT_CONFIDENCE_THRESHOLD,
        private readonly int $maxConcurrentWarming = self::MAX_CONCURRENT_WARMING
    ) {
    }

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

        $this->recordAccess($key, [
            'is_hit' => $isHit,
            'retrieval_time_ms' => $retrievalTime?->toMilliseconds() ?? 0,
            'time_of_day' => (int) Timestamp::now()->format('H'),
            'day_of_week' => (int) Timestamp::now()->format('N'),
        ]);
    }

    public function onCacheSet(CacheKey $key, mixed $value, ?Duration $originalTtl): Duration
    {
        // Predictive strategy doesn't modify TTL
        return $originalTtl ?? Duration::fromHours(1);
    }

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

        $keyString = $key->toString();
        unset($this->patterns[$keyString]);
        unset($this->activeWarmingJobs[$keyString]);
        unset($this->warmingHistory[$keyString]);
        unset($this->warmingCallbacks[$keyString]);
    }

    public function getStats(): array
    {
        $totalPatterns = count($this->patterns);
        $activeJobs = count($this->activeWarmingJobs);
        $completedWarming = count($this->warmingHistory);

        $successfulWarming = 0;
        $totalWarmingTime = 0;

        foreach ($this->warmingHistory as $result) {
            if ($result->successful) {
                $successfulWarming++;
            }
            $totalWarmingTime += $result->duration->toMilliseconds();
        }

        $avgWarmingTime = $completedWarming > 0 ? ($totalWarmingTime / $completedWarming) : 0;
        $successRate = $completedWarming > 0 ? ($successfulWarming / $completedWarming) : 0;

        return [
            'strategy' => 'PredictiveCacheStrategy',
            'enabled' => $this->enabled,
            'total_patterns' => $totalPatterns,
            'active_warming_jobs' => $activeJobs,
            'completed_warming_operations' => $completedWarming,
            'successful_warming_operations' => $successfulWarming,
            'warming_success_rate' => round($successRate, 3),
            'avg_warming_time_ms' => round($avgWarmingTime, 2),
            'confidence_threshold' => $this->confidenceThreshold,
            'prediction_window_hours' => $this->predictionWindowHours,
        ];
    }

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

    public function clear(): void
    {
        $this->patterns = [];
        $this->warmingHistory = [];
        $this->warmingCallbacks = [];
        // Don't clear active jobs as they're still running
    }

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

    /**
     * Record access for pattern learning
     */
    public function recordAccess(CacheKey $key, array $context = []): void
    {
        if (! $this->enabled) {
            return;
        }

        $keyString = $key->toString();
        $timestamp = Timestamp::now();

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

        $this->patterns[$keyString]->recordAccess($timestamp, $context);
    }

    /**
     * Record cache dependency relationship
     */
    public function recordDependency(CacheKey $primaryKey, CacheKey $dependentKey): void
    {
        if (! $this->enabled) {
            return;
        }

        $primaryString = $primaryKey->toString();

        if (! isset($this->patterns[$primaryString])) {
            $this->patterns[$primaryString] = new PredictionPattern($primaryKey);
        }

        $this->patterns[$primaryString]->addDependency($dependentKey);
    }

    /**
     * Generate predictions based on learned patterns
     */
    public function generatePredictions(): array
    {
        if (! $this->enabled) {
            return [];
        }

        $predictions = [];
        $now = Timestamp::now();

        foreach ($this->patterns as $keyString => $pattern) {
            $keyPredictions = $this->predictKeyAccess($pattern, $now);
            $predictions = array_merge($predictions, $keyPredictions);
        }

        // Sort by confidence score
        usort($predictions, fn ($a, $b) => $b['confidence'] <=> $a['confidence']);

        return $predictions;
    }

    /**
     * Perform predictive warming based on patterns
     */
    public function performPredictiveWarming(): array
    {
        if (! $this->enabled) {
            return [];
        }

        $predictions = $this->generatePredictions();
        $warmingResults = [];

        foreach ($predictions as $prediction) {
            if ($prediction['confidence'] >= $this->confidenceThreshold) {
                $result = $this->warmCacheKey(
                    $prediction['key'],
                    $prediction['callback'] ?? null,
                    $prediction['reason']
                );
                $warmingResults[] = $result;

                // Limit concurrent warming operations
                if (count($this->activeWarmingJobs) >= $this->maxConcurrentWarming) {
                    break;
                }
            }
        }

        return $warmingResults;
    }

    /**
     * Register a warming callback for a specific key
     */
    public function registerWarmingCallback(CacheKey $key, callable $callback): void
    {
        if (! $this->enabled) {
            return;
        }

        $keyString = $key->toString();
        $this->warmingCallbacks[$keyString] = $callback;

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

        $this->patterns[$keyString]->setWarmingCallback($callback);
    }

    /**
     * Predict access patterns for a specific key
     */
    private function predictKeyAccess(PredictionPattern $pattern, Timestamp $now): array
    {
        $predictions = [];
        $key = $pattern->getKey();

        // Time-based prediction (daily patterns)
        $timeBasedConfidence = $this->calculateTimeBasedConfidence($pattern, $now);
        if ($timeBasedConfidence > 0.3) {
            $predictions[] = [
                'key' => $key,
                'confidence' => $timeBasedConfidence,
                'reason' => 'time_based_pattern',
                'predicted_access_time' => $now->add(Duration::fromMinutes(30)),
                'callback' => $pattern->getWarmingCallback(),
            ];
        }

        // Frequency-based prediction
        $frequencyConfidence = $this->calculateFrequencyConfidence($pattern, $now);
        if ($frequencyConfidence > 0.4) {
            $predictions[] = [
                'key' => $key,
                'confidence' => $frequencyConfidence,
                'reason' => 'access_frequency_pattern',
                'predicted_access_time' => $now->add(Duration::fromMinutes(15)),
                'callback' => $pattern->getWarmingCallback(),
            ];
        }

        // Dependency-based prediction
        $dependencyPredictions = $this->predictDependencyAccess($pattern, $now);
        $predictions = array_merge($predictions, $dependencyPredictions);

        return $predictions;
    }

    /**
     * Calculate confidence based on time patterns (daily/hourly)
     */
    private function calculateTimeBasedConfidence(PredictionPattern $pattern, Timestamp $now): float
    {
        $accesses = $pattern->getRecentAccesses($this->predictionWindowHours);

        if (count($accesses) < self::MIN_PATTERN_OCCURRENCES) {
            return 0.0;
        }

        $currentHour = (int) $now->format('H');
        $currentDayOfWeek = (int) $now->format('N'); // 1 = Monday, 7 = Sunday

        $hourMatches = 0;
        $dayMatches = 0;
        $totalAccesses = count($accesses);

        foreach ($accesses as $access) {
            $accessHour = (int) $access['timestamp']->format('H');
            $accessDay = (int) $access['timestamp']->format('N');

            // Hour pattern matching (±1 hour tolerance)
            if (abs($accessHour - $currentHour) <= 1) {
                $hourMatches++;
            }

            // Day pattern matching
            if ($accessDay === $currentDayOfWeek) {
                $dayMatches++;
            }
        }

        $hourConfidence = $hourMatches / $totalAccesses;
        $dayConfidence = $dayMatches / $totalAccesses;

        // Combined confidence with hour pattern weighted more heavily
        return ($hourConfidence * 0.7) + ($dayConfidence * 0.3);
    }

    /**
     * Calculate confidence based on access frequency patterns
     */
    private function calculateFrequencyConfidence(PredictionPattern $pattern, Timestamp $now): float
    {
        $accesses = $pattern->getRecentAccesses($this->predictionWindowHours);

        if (count($accesses) < self::MIN_PATTERN_OCCURRENCES) {
            return 0.0;
        }

        // Calculate average time between accesses
        $intervals = [];
        for ($i = 1; $i < count($accesses); $i++) {
            $interval = $accesses[$i]['timestamp']->diff($accesses[$i - 1]['timestamp']);
            $intervals[] = $interval->toSeconds();
        }

        if (empty($intervals)) {
            return 0.0;
        }

        $avgInterval = array_sum($intervals) / count($intervals);
        $lastAccess = end($accesses)['timestamp'];
        $timeSinceLastAccess = $now->diff($lastAccess)->toSeconds();

        // Confidence increases as we approach the expected next access time
        $expectedNextAccess = $avgInterval;
        $confidence = 1.0 - abs($timeSinceLastAccess - $expectedNextAccess) / $expectedNextAccess;

        return max(0.0, min(1.0, $confidence));
    }

    /**
     * Predict access for dependent keys
     */
    private function predictDependencyAccess(PredictionPattern $pattern, Timestamp $now): array
    {
        $predictions = [];
        $dependencies = $pattern->getDependencies();

        if (empty($dependencies)) {
            return $predictions;
        }

        // If the primary key is likely to be accessed, warm dependencies too
        $primaryConfidence = max(
            $this->calculateTimeBasedConfidence($pattern, $now),
            $this->calculateFrequencyConfidence($pattern, $now)
        );

        if ($primaryConfidence > 0.5) {
            foreach ($dependencies as $dependentKey) {
                $predictions[] = [
                    'key' => $dependentKey,
                    'confidence' => $primaryConfidence * 0.8, // Slightly lower confidence for dependencies
                    'reason' => 'dependency_pattern',
                    'predicted_access_time' => $now->add(Duration::fromMinutes(5)),
                    'callback' => $this->warmingCallbacks[$dependentKey->toString()] ?? null,
                ];
            }
        }

        return $predictions;
    }

    /**
     * Warm a specific cache key
     */
    private function warmCacheKey(CacheKey $key, ?callable $callback, string $reason): array
    {
        $startTime = Timestamp::now();
        $keyString = $key->toString();

        // Check if already warming this key
        if (isset($this->activeWarmingJobs[$keyString])) {
            return [
                'key' => $keyString,
                'status' => 'already_warming',
                'reason' => $reason,
                'timestamp' => $startTime->format('H:i:s'),
            ];
        }

        // Create warming job
        $job = new WarmingJob($key, $callback, $reason, $startTime);
        $this->activeWarmingJobs[$keyString] = $job;

        try {
            // If no callback provided, we can't warm the cache
            if (! $callback) {
                return [
                    'key' => $keyString,
                    'status' => 'no_callback',
                    'reason' => $reason,
                    'timestamp' => $startTime->format('H:i:s'),
                ];
            }

            // Execute warming (this would need cache access in real implementation)
            $value = $callback();

            $endTime = Timestamp::now();
            $duration = $startTime->diff($endTime);

            // Record warming result
            $result = new WarmingResult($key, true, $duration, $reason);
            $this->warmingHistory[$keyString] = $result;

            return [
                'key' => $keyString,
                'status' => 'warmed',
                'reason' => $reason,
                'duration_ms' => $duration->toMilliseconds(),
                'timestamp' => $startTime->format('H:i:s'),
            ];

        } catch (\Throwable $e) {
            $endTime = Timestamp::now();
            $duration = $startTime->diff($endTime);

            return [
                'key' => $keyString,
                'status' => 'error',
                'reason' => $reason,
                'error' => $e->getMessage(),
                'duration_ms' => $duration->toMilliseconds(),
                'timestamp' => $startTime->format('H:i:s'),
            ];

        } finally {
            // Remove from active jobs
            unset($this->activeWarmingJobs[$keyString]);
        }
    }
}

/**
 * Prediction pattern for a cache key
 */
final class PredictionPattern
{
    /** @var array<array{timestamp: Timestamp, context: array}> */
    private array $accesses = [];

    /** @var array<CacheKey> */
    private array $dependencies = [];

    private mixed $warmingCallback = null;

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

    public function recordAccess(Timestamp $timestamp, array $context = []): void
    {
        $this->accesses[] = [
            'timestamp' => $timestamp,
            'context' => $context,
        ];

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

    public function addDependency(CacheKey $dependentKey): void
    {
        $this->dependencies[] = $dependentKey;
    }

    public function setWarmingCallback(callable $callback): void
    {
        $this->warmingCallback = $callback;
    }

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

    public function getRecentAccesses(int $hours): array
    {
        $cutoff = Timestamp::now()->subtract(Duration::fromHours($hours));

        return array_filter(
            $this->accesses,
            fn ($access) => $access['timestamp']->isAfter($cutoff)
        );
    }

    public function getDependencies(): array
    {
        return $this->dependencies;
    }

    public function getWarmingCallback(): ?callable
    {
        return $this->warmingCallback;
    }
}

/**
 * Active warming job
 */
final readonly class WarmingJob
{
    public function __construct(
        public CacheKey $key,
        public mixed $callback,
        public string $reason,
        public Timestamp $startTime
    ) {
    }
}

/**
 * Warming operation result
 */
final readonly class WarmingResult
{
    public function __construct(
        public CacheKey $key,
        public bool $successful,
        public Duration $duration,
        public string $reason
    ) {
    }
}
