<?php

declare(strict_types=1);

namespace App\Framework\Cache;

use App\Framework\Cache\Contracts\DriverAccessible;
use App\Framework\Core\ValueObjects\Duration;

final readonly class MultiLevelCache implements Cache, DriverAccessible
{
    private const int MAX_FAST_CACHE_SIZE = 1024; //1KB
    private const int FAST_CACHE_TTL_SECONDS = 300;

    public function __construct(
        private Cache $fastCache,   // z.B. ArrayCache
        private Cache $slowCache    // z.B. RedisCache, FileCache
    ) {
    }

    /**
     * Returns the default TTL for fast cache as a Duration object
     */
    private static function getDefaultFastCacheTTL(): Duration
    {
        return Duration::fromSeconds(self::FAST_CACHE_TTL_SECONDS);
    }

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

        // Try fast cache first
        $fastResult = $this->fastCache->get(...$identifiers);
        $allItems = [];
        $missedIdentifiers = [];

        foreach ($identifiers as $identifier) {
            $item = $fastResult->getItem($identifier instanceof CacheKey ? $identifier : CacheKey::fromString($identifier->toString()));

            if ($item->isHit) {
                $allItems[] = $item;
            } else {
                $missedIdentifiers[] = $identifier;
            }
        }

        // Fallback to slow cache for missed items
        if (! empty($missedIdentifiers)) {
            $slowResult = $this->slowCache->get(...$missedIdentifiers);

            foreach ($slowResult->getItems() as $item) {
                if ($item->isHit && $item->key instanceof CacheKey) {
                    // Cache in fast cache if appropriate
                    if ($this->shouldCacheInFast($item->value)) {
                        $this->fastCache->set(CacheItem::forSet($item->key, $item->value, self::getDefaultFastCacheTTL()));
                    }
                }
                $allItems[] = $item;
            }
        }

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

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

        $slowSuccess = $this->slowCache->set(...$items);

        // Also set in fast cache if appropriate
        $fastItems = [];
        foreach ($items as $item) {
            if ($this->shouldCacheInFast($item->value)) {
                $fastTtl = $item->ttl !== null ?
                    Duration::fromSeconds(min($item->ttl->toSeconds(), self::FAST_CACHE_TTL_SECONDS)) :
                    self::getDefaultFastCacheTTL();
                $fastItems[] = CacheItem::forSet($item->key, $item->value, $fastTtl);
            }
        }

        $fastSuccess = true;
        if (! empty($fastItems)) {
            $fastSuccess = $this->fastCache->set(...$fastItems);
        }

        return $slowSuccess && $fastSuccess;
    }

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

        $s1 = $this->fastCache->forget(...$identifiers);
        $s2 = $this->slowCache->forget(...$identifiers);

        return $s1 && $s2;
    }

    public function clear(): bool
    {
        $s1 = $this->fastCache->clear();
        $s2 = $this->slowCache->clear();

        return $s1 && $s2;
    }

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

        $fastResults = $this->fastCache->has(...$identifiers);
        $results = [];
        $toCheckInSlow = [];

        foreach ($identifiers as $identifier) {
            $keyString = $identifier->toString();
            if ($fastResults[$keyString] ?? false) {
                $results[$keyString] = true;
            } else {
                $toCheckInSlow[] = $identifier;
                $results[$keyString] = false; // Default to false, will be updated if found in slow
            }
        }

        // Check missed items in slow cache and warm up fast cache
        if (! empty($toCheckInSlow)) {
            $slowResults = $this->slowCache->has(...$toCheckInSlow);
            foreach ($toCheckInSlow as $identifier) {
                $keyString = $identifier->toString();
                if ($slowResults[$keyString] ?? false) {
                    $results[$keyString] = true;

                    // Warm up fast cache if it's a key identifier
                    if ($identifier instanceof CacheKey) {
                        $slowResult = $this->slowCache->get($identifier);
                        $item = $slowResult->getItem($identifier);
                        if ($item->isHit && $this->shouldCacheInFast($item->value)) {
                            $this->fastCache->set(CacheItem::forSet($identifier, $item->value, self::getDefaultFastCacheTTL()));
                        }
                    }
                }
            }
        }

        return $results;
    }

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

        // Wert generieren, speichern und zurückgeben
        $value = $callback();
        $this->set(CacheItem::forSet($key, $value, $ttl));

        // Erstelle neuen CacheItem als Treffer
        return CacheItem::hit($key, $value);
    }

    private function shouldCacheInFast(mixed $value): bool
    {
        // Wenn es bereits ein String ist (serialisiert), nutze dessen Länge
        if (is_string($value)) {
            return strlen($value) <= self::MAX_FAST_CACHE_SIZE;
        }

        // Für Arrays: Schnelle Heuristik ohne Serialisierung
        if (is_array($value)) {
            $elementCount = count($value, COUNT_RECURSIVE);
            // Grobe Schätzung: 50 Bytes pro Element
            $estimatedSize = $elementCount * 50;

            return $estimatedSize <= self::MAX_FAST_CACHE_SIZE;
        }

        // Für Objekte: Konservativ - nicht in fast cache
        if (is_object($value)) {
            return false;
        }

        // Primitive Typen: Immer cachen
        return true;
    }

    /**
     * Get the underlying cache driver (uses slow cache as primary)
     */
    public function getDriver(): ?CacheDriver
    {
        // Try slow cache first as it's typically the primary storage
        if ($this->slowCache instanceof DriverAccessible) {
            return $this->slowCache->getDriver();
        }

        // If slow cache doesn't have driver access, try fast cache
        if ($this->fastCache instanceof DriverAccessible) {
            return $this->fastCache->getDriver();
        }

        // Check if the cache layers are directly drivers
        if ($this->slowCache instanceof CacheDriver) {
            return $this->slowCache;
        }

        if ($this->fastCache instanceof CacheDriver) {
            return $this->fastCache;
        }

        return null;
    }

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

        return $driver !== null && $driver instanceof $interface;
    }

    /**
     * Get the slow cache (primary storage layer)
     */
    public function getSlowCache(): Cache
    {
        return $this->slowCache;
    }

    /**
     * Get the fast cache (quick access layer)
     */
    public function getFastCache(): Cache
    {
        return $this->fastCache;
    }
}
