<?php

declare(strict_types=1);

namespace App\Framework\LiveComponents;

use App\Framework\Cache\Cache;
use App\Framework\Cache\CacheItem;
use App\Framework\Cache\CacheKey;
use App\Framework\Cache\CacheTag;
use App\Framework\LiveComponents\Caching\CacheContext;
use App\Framework\LiveComponents\Caching\CacheKeyBuilder;
use App\Framework\LiveComponents\Caching\CacheMetrics;
use App\Framework\LiveComponents\Contracts\Cacheable;
use App\Framework\LiveComponents\Contracts\LiveComponentContract;

/**
 * Component Cache Manager
 *
 * Advanced caching for LiveComponents with:
 * - VaryBy cache key variations (userId, locale, roles, feature flags)
 * - Stale-While-Revalidate (SWR) for serving stale content while refreshing
 * - Cache metrics tracking (hit rate, age, freshness)
 * - Tag-based invalidation
 *
 * SWR Example:
 * - TTL = 5 minutes, SWR = 1 hour
 * - 0-5min: Serve fresh cache (HIT)
 * - 5min-1h: Serve stale cache + trigger background refresh (STALE)
 * - >1h: Force fresh render (MISS)
 */
final readonly class ComponentCacheManager
{
    public function __construct(
        private Cache $cache,
        private CacheKeyBuilder $cacheKeyBuilder,
        private ?CacheContext $cacheContext = null
    ) {
    }

    /**
     * Get cached component HTML with SWR support
     *
     * Returns array with:
     * - 'html': Cached HTML (null if miss)
     * - 'metrics': CacheMetrics object
     * - 'needs_refresh': Whether background refresh is needed (SWR)
     */
    public function getCachedHtml(LiveComponentContract $component): array
    {
        if (! $component instanceof Cacheable || ! $component->shouldCache()) {
            return [
                'html' => null,
                'metrics' => CacheMetrics::miss(),
                'needs_refresh' => false,
            ];
        }

        $cacheKey = $this->buildCacheKey($component);
        $item = $this->cache->get($cacheKey);

        if (! $item || ! isset($item->value['html'])) {
            return [
                'html' => null,
                'metrics' => CacheMetrics::miss($cacheKey->toString()),
                'needs_refresh' => false,
            ];
        }

        $cachedAt = $item->value['cached_at'] ?? time();
        $ttl = $component->getCacheTTL();
        $expiresAt = $cachedAt + $ttl->toSeconds();
        $swrDuration = $component->getStaleWhileRevalidate();

        // Check if content is expired
        $now = time();
        $isExpired = $now > $expiresAt;

        // Fresh cache hit
        if (! $isExpired) {
            return [
                'html' => $item->value['html'],
                'metrics' => CacheMetrics::hit($cachedAt, $expiresAt, $cacheKey->toString()),
                'needs_refresh' => false,
            ];
        }

        // Stale-While-Revalidate: Serve stale content if within SWR window
        if ($swrDuration !== null) {
            $swrExpiresAt = $expiresAt + $swrDuration->toSeconds();
            $isWithinSwrWindow = $now <= $swrExpiresAt;

            if ($isWithinSwrWindow) {
                // Serve stale content and signal background refresh needed
                $isRefreshing = $item->value['refreshing'] ?? false;

                return [
                    'html' => $item->value['html'],
                    'metrics' => CacheMetrics::staleHit($cachedAt, $expiresAt, $cacheKey->toString(), $isRefreshing),
                    'needs_refresh' => ! $isRefreshing,
                ];
            }
        }

        // Outside SWR window: cache miss
        return [
            'html' => null,
            'metrics' => CacheMetrics::miss($cacheKey->toString()),
            'needs_refresh' => false,
        ];
    }

    /**
     * Cache component HTML and state
     */
    public function cacheComponent(
        LiveComponentContract $component,
        string $html,
        array $stateArray
    ): bool {
        if (! $component instanceof Cacheable || ! $component->shouldCache()) {
            return false;
        }

        $cacheKey = $this->buildCacheKey($component);
        $tags = $this->buildCacheTags($component);

        $cacheItem = CacheItem::forSet(
            key: $cacheKey,
            value: [
                'html' => $html,
                'state' => $stateArray,
                'component_id' => $component->id->toString(),
                'cached_at' => time(),
                'refreshing' => false,
            ],
            ttl: $component->getCacheTTL()
        );

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

        // Tag the cached item if successful and we have tags
        if ($success && ! empty($tags)) {
            $this->cache->tag($cacheKey, ...$tags);
        }

        return $success;
    }

    /**
     * Mark component cache as refreshing (for SWR background refresh)
     */
    public function markAsRefreshing(LiveComponentContract $component): bool
    {
        if (! $component instanceof Cacheable) {
            return false;
        }

        $cacheKey = $this->buildCacheKey($component);
        $item = $this->cache->get($cacheKey);

        if (! $item) {
            return false;
        }

        // Update refreshing flag
        $value = $item->value;
        $value['refreshing'] = true;

        $updatedItem = CacheItem::forSet(
            key: $cacheKey,
            value: $value,
            ttl: $component->getCacheTTL()
        );

        return $this->cache->set($updatedItem);
    }

    /**
     * Invalidate cached component
     */
    public function invalidate(LiveComponentContract $component): bool
    {
        if (! $component instanceof Cacheable) {
            return false;
        }

        $cacheKey = $this->buildCacheKey($component);

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

    /**
     * Invalidate all components with specific tag
     */
    public function invalidateByTag(string $tag): bool
    {
        $cacheTag = CacheTag::fromString($tag);

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

    /**
     * Invalidate all components with multiple tags
     */
    public function invalidateByTags(array $tags): bool
    {
        $cacheTags = array_map(
            fn ($tag) => CacheTag::fromString($tag),
            $tags
        );

        foreach ($cacheTags as $tag) {
            $this->cache->forget($tag);
        }

        return true;
    }

    /**
     * Clear all component caches
     */
    public function clearAll(): bool
    {
        // Invalidate by common tag
        return $this->invalidateByTag('livecomponents');
    }

    /**
     * Get cache statistics with metrics for component
     */
    public function getStats(LiveComponentContract $component): array
    {
        if (! $component instanceof Cacheable) {
            return [
                'cacheable' => false,
                'cached' => false,
                'metrics' => null,
            ];
        }

        $cacheKey = $this->buildCacheKey($component);
        $item = $this->cache->get($cacheKey);

        if (! $item) {
            $metrics = CacheMetrics::miss($cacheKey->toString());

            return [
                'cacheable' => true,
                'cached' => false,
                'cache_key' => $cacheKey->toString(),
                'ttl' => $component->getCacheTTL()->toSeconds(),
                'swr_ttl' => $component->getStaleWhileRevalidate()?->toSeconds(),
                'metrics' => $metrics->toArray(),
            ];
        }

        $cachedAt = $item->value['cached_at'] ?? time();
        $ttl = $component->getCacheTTL();
        $expiresAt = $cachedAt + $ttl->toSeconds();
        $swrDuration = $component->getStaleWhileRevalidate();

        // Determine metrics based on freshness
        $now = time();
        $isExpired = $now > $expiresAt;

        if (! $isExpired) {
            $metrics = CacheMetrics::hit($cachedAt, $expiresAt, $cacheKey->toString());
        } else {
            $isRefreshing = $item->value['refreshing'] ?? false;
            $metrics = CacheMetrics::staleHit($cachedAt, $expiresAt, $cacheKey->toString(), $isRefreshing);
        }

        return [
            'cacheable' => true,
            'cached' => true,
            'cache_key' => $cacheKey->toString(),
            'ttl' => $ttl->toSeconds(),
            'swr_ttl' => $swrDuration?->toSeconds(),
            'metrics' => $metrics->toArray(),
        ];
    }

    /**
     * Build cache key for component with varyBy support
     *
     * Examples:
     * - Global: livecomponent:user-stats:total-count:global
     * - Per user: livecomponent:user-stats:total-count:u:123:l:en
     * - With feature flags: livecomponent:dashboard:widgets:u:456:f:new-ui,dark-mode
     */
    private function buildCacheKey(Cacheable $component): CacheKey
    {
        $componentName = $this->getComponentName($component);
        $baseKey = $component->getCacheKey();
        $varyBy = $component->getVaryBy();

        return $this->cacheKeyBuilder->build(
            componentName: $componentName,
            baseKey: $baseKey,
            varyBy: $varyBy,
            context: $this->cacheContext
        );
    }

    /**
     * Extract component name from class
     */
    private function getComponentName(LiveComponentContract $component): string
    {
        $className = $component::class;
        $shortName = substr($className, strrpos($className, '\\') + 1);

        // Convert PascalCase to kebab-case
        return strtolower(preg_replace('/([a-z])([A-Z])/', '$1-$2', $shortName));
    }

    /**
     * Build cache tags for component
     */
    private function buildCacheTags(Cacheable $component): array
    {
        $tags = ['livecomponents'];

        foreach ($component->getCacheTags() as $tag) {
            $tags[] = $tag;
        }

        return array_map(
            fn ($tag) => CacheTag::fromString($tag),
            $tags
        );
    }
}
