<?php

declare(strict_types=1);

namespace App\Framework\Cache;

use App\Framework\Cache\Compression\NoCompression;
use App\Framework\Cache\Contracts\DriverAccessible;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Serializer\Serializer;

final readonly class GeneralCache implements Cache, DriverAccessible
{
    private const int COMPRESSION_THRESHOLD = 1024; // 1KB threshold for compression

    public function __construct(
        private CacheDriver $adapter,
        private Serializer $serializer,
        private CompressionAlgorithm $compressionAlgorithm = new NoCompression(),
        private bool $autoCompress = true
    ) {
    }

    public function get(CacheIdentifier ...$identifiers): CacheResult
    {
        if (empty($identifiers)) {
            return CacheResult::empty();
        }

        // GeneralCache only supports CacheKey identifiers
        // For pattern/prefix/tag support, use SmartCache instead
        $keys = [];
        foreach ($identifiers as $identifier) {
            if ($identifier instanceof CacheKey) {
                $keys[] = $identifier;
            }
            // Ignore non-key identifiers - they're not supported in GeneralCache
        }

        if (empty($keys)) {
            return CacheResult::empty();
        }

        // Get items from adapter
        $result = $this->adapter->get(...$keys);
        $items = $result->getItems();

        // Apply decompression (NoCompression will be no-op)
        $items = $this->decompressItems($items);

        return CacheResult::fromItems(...$items);
    }

    public function set(CacheItem ...$items): bool
    {
        if (empty($items)) {
            return true;
        }

        // EMERGENCY: Monitor cache sizes
        foreach ($items as $item) {
            $this->monitorCacheSize($item);
        }

        // Apply compression (NoCompression will be no-op)
        $items = $this->compressItems($items);

        // CacheDriver now accepts CacheItem objects directly
        return $this->adapter->set(...$items);
    }

    public function has(CacheIdentifier ...$identifiers): array
    {
        if (empty($identifiers)) {
            return [];
        }

        // GeneralCache only supports CacheKey identifiers
        // For pattern/prefix/tag support, use SmartCache instead
        $keys = [];
        foreach ($identifiers as $identifier) {
            if ($identifier instanceof CacheKey) {
                $keys[] = $identifier;
            }
            // Ignore non-key identifiers - they're not supported in GeneralCache
        }

        if (empty($keys)) {
            return [];
        }

        // Direct adapter call for key existence check
        return $this->adapter->has(...$keys);
    }

    public function forget(CacheIdentifier ...$identifiers): bool
    {
        if (empty($identifiers)) {
            return true;
        }

        // GeneralCache only supports CacheKey identifiers
        // For pattern/prefix/tag support, use SmartCache instead
        $keys = [];
        foreach ($identifiers as $identifier) {
            if ($identifier instanceof CacheKey) {
                $keys[] = $identifier;
            }
            // Ignore non-key identifiers - they're not supported in GeneralCache
        }

        if (empty($keys)) {
            return true;
        }

        // Direct adapter call for key deletion
        return $this->adapter->forget(...$keys);
    }

    public function clear(): bool
    {
        return $this->adapter->clear();
    }

    public function remember(CacheKey $key, callable $callback, ?Duration $ttl = null): CacheItem
    {
        $result = $this->get($key);
        $item = $result->getItem($key);

        if ($item->isHit) {
            return $item;
        }

        $value = $callback();
        $this->set(CacheItem::forSet($key, $value, $ttl));

        return CacheItem::hit($key, $value, $ttl);
    }

    /**
     * Compress cache items before storage
     *
     * @param array<CacheItem> $items
     * @return array<CacheItem>
     */
    private function compressItems(array $items): array
    {
        $processedItems = [];

        foreach ($items as $item) {
            $value = $item->value;

            // Always serialize non-string values
            if (! is_string($value)) {
                $value = $this->serializer->serialize($value);
            }

            // Apply compression if beneficial (NoCompression will return unchanged)
            if ($this->shouldCompress($value)) {
                $value = $this->compressionAlgorithm->compress($value);
            }

            $processedItems[] = CacheItem::forSet($item->key, $value, $item->ttl);
        }

        return $processedItems;
    }

    /**
     * Decompress cache items after retrieval
     *
     * @param array<CacheItem> $items
     * @return array<CacheItem>
     */
    private function decompressItems(array $items): array
    {
        $processedItems = [];

        foreach ($items as $item) {
            if (! $item->isHit || ! is_string($item->value)) {
                $processedItems[] = $item;

                continue;
            }

            $value = $item->value;

            // Always attempt decompression (NoCompression will return unchanged)
            if ($this->compressionAlgorithm->isCompressed($value)) {
                $value = $this->compressionAlgorithm->decompress($value);
            }

            try {
                // Always attempt deserialization
                $unserialized = $this->serializer->deserialize($value);
                $processedItems[] = CacheItem::hit($item->key, $unserialized, $item->ttl);
            } catch (\Throwable $e) {
                // Keep as string if deserialization fails (expected for plain strings)
                $processedItems[] = CacheItem::hit($item->key, $value, $item->ttl);
            }
        }

        return $processedItems;
    }

    /**
     * Determine if a value should be compressed
     */
    private function shouldCompress(string $value): bool
    {
        if (! $this->autoCompress) {
            return false;
        }

        // Only compress if value is above threshold
        return strlen($value) >= self::COMPRESSION_THRESHOLD;
    }

    /**
     * Get the underlying cache driver (implements DriverAccessible)
     */
    public function getDriver(): ?CacheDriver
    {
        return $this->adapter;
    }

    /**
     * Check if the underlying driver supports a specific interface
     */
    public function driverSupports(string $interface): bool
    {
        return $this->adapter instanceof $interface;
    }

    /**
     * Check if compression is enabled
     */
    public function hasCompressionEnabled(): bool
    {
        return ! ($this->compressionAlgorithm instanceof NoCompression);
    }

    /**
     * Get compression statistics
     */
    public function getCompressionStats(): array
    {
        return [
            'compression_enabled' => $this->hasCompressionEnabled(),
            'auto_compress' => $this->autoCompress,
            'compression_threshold' => self::COMPRESSION_THRESHOLD,
            'algorithm' => get_class($this->compressionAlgorithm),
            'serializer' => get_class($this->serializer),
        ];
    }

    /**
     * EMERGENCY: Monitor cache item sizes to identify memory explosion sources
     */
    private function monitorCacheSize(CacheItem $item): void
    {
        try {
            // Get serialized size to understand cache impact
            $serializedValue = serialize($item->value);
            $sizeBytes = strlen($serializedValue);
            $sizeMB = round($sizeBytes / 1024 / 1024, 2);

            // Log large cache items
            if ($sizeBytes > 50 * 1024) { // >50KB for GeneralCache
                $keyString = $item->key->toString();
                $valueType = get_debug_type($item->value);

                error_log("⚠️ GENERAL CACHE SIZE WARNING: {$sizeMB}MB");
                error_log("   📋 Cache Key: '{$keyString}'");
                error_log("   🏷️  Value Type: {$valueType}");

                // If it's an object, get class name
                if (is_object($item->value)) {
                    $className = get_class($item->value);
                    error_log("   🔍 Object Class: {$className}");
                }

                // Show cache key pattern analysis
                $keyAnalysis = $this->analyzeCacheKeyPattern($keyString);
                error_log("   🔑 Key Pattern: {$keyAnalysis}");
            }

            // EMERGENCY: Block extremely large cache items
            if ($sizeBytes > 5 * 1024 * 1024) { // >5MB for GeneralCache (more strict)
                error_log("🛑 GENERAL CACHE BLOCK: Refusing to cache {$sizeMB}MB item");
                error_log("   📋 Blocked Key: '{$keyString}'");
                error_log("   🚫 Reason: Exceeds 5MB limit");

                throw new \RuntimeException("GeneralCache item too large: {$sizeMB}MB (max 5MB)");
            }

        } catch (\Throwable $e) {
            // Don't break caching, just log the monitoring error
            error_log("GeneralCache size monitoring failed: " . $e->getMessage());
        }
    }

    /**
     * Analyze cache key patterns to identify system components
     */
    private function analyzeCacheKeyPattern(string $key): string
    {
        $patterns = [];

        // Identify common patterns
        if (str_contains($key, 'discovery')) {
            $patterns[] = 'DISCOVERY_SYSTEM';
        }

        if (str_contains($key, 'reflection')) {
            $patterns[] = 'REFLECTION_CACHE';
        }

        if (str_contains($key, 'container') || str_contains($key, 'di_')) {
            $patterns[] = 'DEPENDENCY_INJECTION';
        }

        if (str_contains($key, 'route')) {
            $patterns[] = 'ROUTING_SYSTEM';
        }

        if (str_contains($key, 'compiled') || str_contains($key, 'compilation')) {
            $patterns[] = 'COMPILED_DATA';
        }

        if (str_contains($key, 'template')) {
            $patterns[] = 'TEMPLATE_SYSTEM';
        }

        if (str_contains($key, 'meta')) {
            $patterns[] = 'METADATA_SYSTEM';
        }

        if (empty($patterns)) {
            $patterns[] = 'UNKNOWN_PATTERN';
        }

        return implode(', ', $patterns);
    }
}
