<?php

declare(strict_types=1);

namespace App\Framework\Discovery\Storage;

use App\Framework\Cache\Cache;
use App\Framework\Cache\CacheItem;
use App\Framework\Cache\CacheKey;
use App\Framework\Cache\CachePrefix;
use App\Framework\Core\Events\EventDispatcher;
use App\Framework\Core\ValueObjects\Byte;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Core\ValueObjects\Score;
use App\Framework\DateTime\Clock;
use App\Framework\Discovery\Events\CacheCompressionEvent;
use App\Framework\Discovery\Events\CacheHitEvent;
use App\Framework\Discovery\Events\CacheMissEvent;
use App\Framework\Discovery\Memory\DiscoveryMemoryManager;
use App\Framework\Discovery\Memory\MemoryStatus;
use App\Framework\Discovery\Results\DiscoveryRegistry;
use App\Framework\Discovery\ValueObjects\CacheLevel;
use App\Framework\Discovery\ValueObjects\CacheMetrics;
use App\Framework\Discovery\ValueObjects\CacheTier;
use App\Framework\Discovery\ValueObjects\CompressionLevel;
use App\Framework\Discovery\ValueObjects\DiscoveryContext;
use App\Framework\Filesystem\ValueObjects\FilePath;
use App\Framework\Filesystem\FileSystemService;
use App\Framework\Logging\Logger;
use App\Framework\Logging\ValueObjects\LogContext;

/**
 * Enhanced Discovery cache manager with memory-aware and tiered caching strategies
 *
 * Combines traditional discovery caching with:
 * - Memory-aware compression and eviction
 * - Tiered caching for different access patterns
 * - Automatic cache level adjustment based on memory pressure
 * - Comprehensive cache metrics and monitoring
 */
final class DiscoveryCacheManager
{
    private const string CACHE_PREFIX = 'discovery:';
    private const int DEFAULT_TTL_HOURS = 24;

    /** @var array<string, array<string, mixed>> */
    private array $cacheMetrics = [];

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

    private CacheLevel $currentLevel = CacheLevel::NORMAL;

    public function __construct(
        private readonly Cache                   $cache,
        private readonly Clock                   $clock,
        private readonly FileSystemService       $fileSystemService,
        private readonly ?Logger                 $logger = null,
        private readonly int                     $ttlHours = self::DEFAULT_TTL_HOURS,
        private readonly ?DiscoveryMemoryManager $memoryManager = null,
        private readonly ?EventDispatcher        $eventDispatcher = null,
        private float                            $compressionThreshold = 0.75,
        private readonly float $evictionThreshold = 0.85
    ) {
    }

    /**
     * Get cached discovery results with memory-aware retrieval
     */
    public function get(DiscoveryContext $context): ?DiscoveryRegistry
    {
        // Try tiered cache first
        $result = $this->getTieredCache($context);
        if ($result !== null) {
            return $result;
        }

        // Fallback to standard cache
        $key = $this->buildCacheKey($context);
        $result = $this->cache->get($key);
        $item = $result->getItem($key);

        if (! $item->isHit) {
            $this->emitCacheMissEvent($key->toString(), 'not_found');

            return null;
        }

        $cached = $this->processRetrievedData($item->value);
        if (! $cached instanceof DiscoveryRegistry) {
            $this->logger?->warning('Invalid cache data type', LogContext::withData([
                'key' => $key->toString(),
                'type' => gettype($cached),
            ]));
            $this->emitCacheMissEvent($key->toString(), 'corrupted');

            return null;
        }

        // Validate cache freshness
        if ($this->isStale($context, $cached)) {
            $this->logger?->info('Discovery cache is stale', LogContext::withData([
                'key' => $key->toString(),
            ]));
            $this->invalidate($context);
            $this->emitCacheMissEvent($key->toString(), 'expired');

            return null;
        }

        // Track access and emit hit event
        $this->trackAccess($key->toString());
        $dataSize = $this->estimateRegistrySize($cached);
        $this->emitCacheHitEvent($key->toString(), $dataSize);

        return $cached;
    }

    /**
     * Store discovery results with memory-aware and tiered caching
     */
    public function store(DiscoveryContext $context, DiscoveryRegistry $registry): bool
    {
        // Optimize registry before caching
        $registry->optimize();

        // Determine cache strategy based on memory status
        if ($this->memoryManager !== null) {
            return $this->storeWithMemoryAwareness($context, $registry);
        }

        // Fallback to standard caching
        return $this->storeStandard($context, $registry);
    }

    /**
     * Store with memory-aware strategy
     */
    private function storeWithMemoryAwareness(DiscoveryContext $context, DiscoveryRegistry $registry): bool
    {
        $memoryStatus = $this->memoryManager->getMemoryStatus('cache_store');
        $dataSize = $this->estimateRegistrySize($registry);

        // Determine appropriate cache level and tier
        $cacheLevel = $this->determineCacheLevel($memoryStatus);
        $cacheTier = $this->determineCacheTier($context, $registry, $memoryStatus);

        // Apply memory-aware processing
        $processedData = $this->applyMemoryProcessing($registry, $cacheLevel, $cacheTier);
        $adjustedTtl = $this->calculateAdjustedTtl($cacheTier, $memoryStatus);

        // Store in appropriate tier
        $key = $this->buildTieredCacheKey($context, $cacheTier);
        $cacheItem = CacheItem::forSet(
            key: $key,
            value: $processedData,
            ttl: $adjustedTtl
        );

        $success = $this->cache->set($cacheItem);

        if ($success) {
            $this->updateCacheMetrics($key->toString(), $dataSize, $cacheLevel, 'store');
            $this->trackAccess($key->toString());

            $this->logger?->info('Discovery results cached with memory awareness', LogContext::withData([
                'key' => $key->toString(),
                'items' => count($registry),
                'cache_level' => $cacheLevel->value,
                'cache_tier' => $cacheTier->value,
                'memory_pressure' => $memoryStatus->memoryPressure->toDecimal(),
                'data_size' => $dataSize->toHumanReadable(),
                'ttl' => $adjustedTtl->toHours(),
            ]));
        }

        return $success;
    }

    /**
     * Standard storage method (fallback)
     */
    private function storeStandard(DiscoveryContext $context, DiscoveryRegistry $registry): bool
    {
        $key = $this->buildCacheKey($context);
        $ttl = Duration::fromHours($this->ttlHours);

        $cacheItem = CacheItem::forSet(
            key: $key,
            value: $registry,
            ttl: $ttl
        );

        $success = $this->cache->set($cacheItem);

        if ($success) {
            $this->logger?->info('Discovery results cached (standard)', LogContext::withData([
                'key' => $key->toString(),
                'items' => count($registry),
                'ttl' => $ttl->toHours(),
            ]));
        }

        return $success;
    }

    /**
     * Invalidate cache for a context
     */
    public function invalidate(DiscoveryContext $context): bool
    {
        $key = $this->buildCacheKey($context);

        return $this->cache->forget($key);
    }

    /**
     * Clear all discovery caches
     */
    public function clearAll(): bool
    {
        // Clear only discovery-prefixed cache entries
        $this->logger?->info('Clearing all discovery caches');

        $prefix = CachePrefix::fromString(self::CACHE_PREFIX);
        return $this->cache->forget($prefix);
    }

    /**
     * Get enhanced cache health status with memory awareness
     *
     * @return array<string, mixed>
     */
    public function getHealthStatus(): array
    {
        /** @var array<string, mixed> */
        $baseStatus = [
            'cache_driver' => get_class($this->cache),
            'ttl_hours' => $this->ttlHours,
            'prefix' => self::CACHE_PREFIX,
            'memory_aware' => $this->memoryManager !== null,
        ];

        if ($this->memoryManager !== null) {
            $memoryStatus = $this->memoryManager->getMemoryStatus('health_check');
            $baseStatus['memory_management'] = [
                'status' => $memoryStatus->status->value,
                'current_usage' => $memoryStatus->currentUsage->toHumanReadable(),
                'memory_pressure' => $memoryStatus->memoryPressure->toDecimal(),
                'cache_level' => $this->currentLevel->value,
            ];
        }

        return $baseStatus;
    }

    /**
     * Get comprehensive cache metrics
     */
    public function getCacheMetrics(): ?CacheMetrics
    {
        if ($this->memoryManager === null) {
            return null;
        }

        $memoryStatus = $this->memoryManager->getMemoryStatus('metrics');
        $hitRate = $this->calculateHitRate();
        $totalSize = $this->calculateTotalCacheSize();
        $compressionRatio = $this->calculateCompressionRatio();

        return new CacheMetrics(
            memoryStatus: $memoryStatus,
            cacheLevel: $this->currentLevel,
            totalItems: count($this->cacheMetrics),
            hitRate: Score::fromRatio((int) ($hitRate * 100), 100),
            totalSize: $totalSize,
            compressionRatio: Score::fromRatio((int) ($compressionRatio * 100), 100),
            evictionCount: 0, // Would need to track this
            timestamp: $this->clock->time()
        );
    }

    /**
     * Perform memory pressure management
     *
     * @return array<string, mixed>
     */
    public function performMemoryPressureManagement(): array
    {
        if ($this->memoryManager === null) {
            return ['message' => 'Memory management not available'];
        }

        $memoryStatus = $this->memoryManager->getMemoryStatus('pressure_management');
        /** @var array<int, string> */
        $actions = [];

        if ($memoryStatus->status === MemoryStatus::CRITICAL) {
            // Clear all caches in critical situations
            $this->clearAll();
            $actions[] = 'emergency_cache_clear';
        } elseif ($memoryStatus->memoryPressure->toDecimal() > $this->evictionThreshold) {
            // Selective eviction based on access patterns
            $evicted = $this->performSelectiveEviction();
            $actions[] = "evicted_{$evicted}_items";
        }

        // Adjust cache level based on memory pressure
        $newLevel = CacheLevel::fromMemoryPressure($memoryStatus->memoryPressure->toDecimal());
        if ($newLevel !== $this->currentLevel) {
            $this->currentLevel = $newLevel;
            $actions[] = "level_changed_to_{$newLevel->value}";
        }

        return [
            'actions' => $actions,
            'memory_status' => $memoryStatus->toArray(),
            'cache_level' => $this->currentLevel->value,
        ];
    }

    /**
     * Build cache key for context
     */
    private function buildCacheKey(DiscoveryContext $context): CacheKey
    {
        // Context returns already a CacheKey, so we use it directly
        return $context->getCacheKey();
    }

    /**
     * Check if cached data is stale
     */
    private function isStale(DiscoveryContext $context, DiscoveryRegistry $registry): bool
    {
        // For incremental scans, always consider cache stale
        if ($context->isIncremental()) {
            return true;
        }

        // Check if any source directories have been modified
        foreach ($context->paths as $path) {
            if ($this->directoryModifiedSince($path, $context->startTime)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Check if directory has been modified since given time
     */
    private function directoryModifiedSince(string $path, \DateTimeInterface $since): bool
    {
        try {
            $filePath = FilePath::create($path);
            $metadata = $this->fileSystemService->getMetadata($filePath);

            // Check if modification time is after the given time
            return $metadata->lastModified > $since->getTimestamp();
        } catch (\Throwable) {
            // If we can't check, assume it's been modified
            return true;
        }
    }

    // === Memory-Aware Helper Methods ===

    /**
     * Get cache from tiered storage
     */
    private function getTieredCache(DiscoveryContext $context): ?DiscoveryRegistry
    {
        if ($this->memoryManager === null) {
            return null;
        }

        // Try each tier in order of preference
        foreach (CacheTier::orderedByPriority() as $tier) {
            $key = $this->buildTieredCacheKey($context, $tier);
            $result = $this->cache->get($key);
            $item = $result->getItem($key);

            if ($item->isHit) {
                $data = $this->processRetrievedData($item->value);
                if ($data instanceof DiscoveryRegistry) {
                    $this->trackAccess($key->toString());

                    return $data;
                }
            }
        }

        return null;
    }

    /**
     * Determine cache level based on memory status
     */
    private function determineCacheLevel(object $memoryStatus): CacheLevel
    {
        return CacheLevel::fromMemoryPressure($memoryStatus->memoryPressure->toDecimal());
    }

    /**
     * Determine cache tier based on context and memory status
     */
    private function determineCacheTier(DiscoveryContext $context, DiscoveryRegistry $registry, object $memoryStatus): CacheTier
    {
        $dataSize = $this->estimateRegistrySize($registry)->toBytes();
        $accessFrequency = $this->getAccessFrequency($context->getCacheKey()->toString());
        $memoryPressure = $memoryStatus->memoryPressure->toDecimal();

        return CacheTier::suggest($dataSize, $accessFrequency, $memoryPressure);
    }

    /**
     * Apply memory-aware processing (compression, etc.)
     */
    private function applyMemoryProcessing(DiscoveryRegistry $registry, CacheLevel $level, CacheTier $tier): mixed
    {
        $compressionLevel = $tier->getCompressionLevel();

        if ($compressionLevel === CompressionLevel::NONE) {
            return $registry;
        }

        // Serialize registry for compression
        $serialized = serialize($registry);
        $originalSize = Byte::fromBytes(strlen($serialized));

        if ($originalSize->lessThan(Byte::fromBytes($compressionLevel->getMinimumDataSize()))) {
            return $registry;
        }

        $compressed = $this->compressData($serialized, $compressionLevel);
        if ($compressed !== null) {
            $compressedSize = Byte::fromBytes(strlen($compressed));

            // Emit compression event
            $this->emitCompressionEvent($originalSize, $compressedSize, $compressionLevel);

            /** @var array<string, mixed> */
            return [
                '__discovery_compressed__' => true,
                'data' => $compressed,
                'level' => $compressionLevel->value,
                'tier' => $tier->value,
            ];
        }

        return $registry;
    }

    /**
     * Process retrieved data (decompress if needed)
     */
    private function processRetrievedData(mixed $data): mixed
    {
        if (! is_array($data) || ! ($data['__discovery_compressed__'] ?? false)) {
            return $data;
        }

        $compressionLevel = CompressionLevel::from($data['level']);
        $decompressed = $this->decompressData($data['data'], $compressionLevel);

        if ($decompressed !== null) {
            return unserialize($decompressed);
        }

        return $data;
    }

    /**
     * Calculate adjusted TTL based on tier and memory status
     */
    private function calculateAdjustedTtl(CacheTier $tier, object $memoryStatus): Duration
    {
        $baseTtl = Duration::fromHours($this->ttlHours);
        $tierMultiplier = $tier->getTtlMultiplier();
        $pressureAdjustment = 1.0 - ($memoryStatus->memoryPressure->toDecimal() * 0.3);

        $finalMultiplier = $tierMultiplier * max(0.1, $pressureAdjustment);

        return Duration::fromSeconds((int) ($baseTtl->toSeconds() * $finalMultiplier));
    }

    /**
     * Build tiered cache key
     */
    private function buildTieredCacheKey(DiscoveryContext $context, CacheTier $tier): CacheKey
    {
        return CacheKey::fromString(self::CACHE_PREFIX . "tier_{$tier->value}:" . $context->getCacheKey()->toString());
    }

    /**
     * Estimate registry size
     */
    private function estimateRegistrySize(DiscoveryRegistry $registry): Byte
    {
        return Byte::fromBytes(strlen(serialize($registry)));
    }

    /**
     * Track access patterns
     */
    private function trackAccess(string $key): void
    {
        $timestamp = $this->clock->time();

        if (! isset($this->accessPatterns[$key])) {
            $this->accessPatterns[$key] = [
                'access_count' => 0,
                'last_access' => $timestamp,
                'access_history' => [],
            ];
        }

        $this->accessPatterns[$key]['access_count']++;
        $this->accessPatterns[$key]['last_access'] = $timestamp;
        $this->accessPatterns[$key]['access_history'][] = $timestamp;

        // Trim history to prevent memory growth
        if (count($this->accessPatterns[$key]['access_history']) > 100) {
            $this->accessPatterns[$key]['access_history'] = array_slice(
                $this->accessPatterns[$key]['access_history'],
                -50
            );
        }
    }

    /**
     * Get access frequency for a key
     */
    private function getAccessFrequency(string $key): float
    {
        if (! isset($this->accessPatterns[$key])) {
            return 0.0;
        }

        $pattern = $this->accessPatterns[$key];
        $timeWindow = 3600; // 1 hour
        $currentTime = $this->clock->time();

        /** @var array<int, int> */
        $recentAccesses = array_filter(
            $pattern['access_history'],
            fn ($timestamp) => ($currentTime - $timestamp) <= $timeWindow
        );

        return count($recentAccesses) / ($timeWindow / 3600);
    }

    /**
     * Update cache metrics
     */
    private function updateCacheMetrics(string $key, Byte $size, CacheLevel $level, string $operation): void
    {
        if (! isset($this->cacheMetrics[$key])) {
            $this->cacheMetrics[$key] = [
                'size' => $size,
                'level' => $level,
                'hits' => 0,
                'misses' => 0,
                'created' => $this->clock->time(),
            ];
        }

        $this->cacheMetrics[$key]['last_access'] = $this->clock->time();

        match ($operation) {
            'hit' => $this->cacheMetrics[$key]['hits']++,
            'miss' => $this->cacheMetrics[$key]['misses']++,
            default => null
        };
    }

    /**
     * Calculate hit rate
     */
    private function calculateHitRate(): float
    {
        $totalHits = array_sum(array_column($this->cacheMetrics, 'hits'));
        $totalMisses = array_sum(array_column($this->cacheMetrics, 'misses'));
        $total = $totalHits + $totalMisses;

        return $total > 0 ? $totalHits / $total : 0.0;
    }

    /**
     * Calculate total cache size
     */
    private function calculateTotalCacheSize(): Byte
    {
        $totalSize = Byte::zero();
        foreach ($this->cacheMetrics as $metrics) {
            $totalSize = $totalSize->add($metrics['size']);
        }

        return $totalSize;
    }

    /**
     * Calculate compression ratio
     */
    private function calculateCompressionRatio(): float
    {
        // Placeholder - would need to track actual compression ratios
        return 0.6; // Assume 40% compression on average
    }

    /**
     * Perform selective eviction
     */
    private function performSelectiveEviction(): int
    {
        // Placeholder - would implement LRU-based eviction
        return 0;
    }

    /**
     * Compress data
     */
    private function compressData(string $data, CompressionLevel $level): ?string
    {
        $compressed = gzcompress($data, $level->getGzipLevel());

        return $compressed !== false ? $compressed : null;
    }

    /**
     * Decompress data
     */
    private function decompressData(string $data, CompressionLevel $level): ?string
    {
        $decompressed = gzuncompress($data);

        return $decompressed !== false ? $decompressed : null;
    }

    /**
     * Emit cache hit event
     */
    private function emitCacheHitEvent(string $key, Byte $dataSize): void
    {
        $this->eventDispatcher?->dispatch(new CacheHitEvent(
            cacheKey: CacheKey::fromString($key),
            itemCount: 1,
            cacheAge: Duration::fromSeconds(0), // Would need actual age
            timestamp: $this->clock->time(),
        ));
    }

    /**
     * Emit cache miss event
     */
    private function emitCacheMissEvent(string $key, string $reason): void
    {
        $this->eventDispatcher?->dispatch(new CacheMissEvent(
            cacheKey: CacheKey::fromString($key),
            reason: $reason,
            cacheLevel: $this->currentLevel,
            timestamp: $this->clock->time()
        ));
    }

    /**
     * Emit compression event
     */
    private function emitCompressionEvent(Byte $originalSize, Byte $compressedSize, CompressionLevel $level): void
    {
        $this->eventDispatcher?->dispatch(new CacheCompressionEvent(
            originalSize: $originalSize,
            compressedSize: $compressedSize,
            compressionLevel: $level,
            compressionRatio: $compressedSize->percentOf($originalSize)->toDecimal(),
            timestamp: $this->clock->time()
        ));
    }
}
