<?php

declare(strict_types=1);

namespace App\Framework\Reflection\Cache;

use App\Framework\Cache\Cache;
use App\Framework\Cache\CacheItem;
use App\Framework\Cache\CacheKey;
use App\Framework\Core\ValueObjects\ClassName;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Core\ValueObjects\Statistics;
use ReflectionClass;

/**
 * Metadata cache for lightweight class information
 *
 * Uses Framework's Cache system for cross-request persistence
 */
final class MetadataCacheManager implements \App\Framework\Reflection\Contracts\ReflectionCache
{
    private const int CACHE_VERSION = 2;
    private const int MAX_CACHE_AGE = 86400; // 24 hours
    private const string CACHE_PREFIX = 'reflection_metadata:';

    public function __construct(
        private readonly Cache $cache
    ) {
    }

    /**
     * Get cached metadata or create from ReflectionClass
     *
     * @return array<string, mixed>
     */
    public function getMetadata(ClassName $className, ReflectionClass $reflectionClass): array
    {
        $cacheKey = $this->getCacheKeyObject($className);

        // Use remember pattern for atomic get-or-create
        $cacheItem = $this->cache->remember(
            $cacheKey,
            fn () => $this->extractMetadata($reflectionClass),
            Duration::fromSeconds(self::MAX_CACHE_AGE)
        );

        // Deserialize if needed (GeneralCache stores non-strings as serialized)
        $metadata = is_string($cacheItem->value) ? unserialize($cacheItem->value) : $cacheItem->value;

        if (! is_array($metadata)) {
            // Fallback if cache returns unexpected data
            return $this->extractMetadata($reflectionClass);
        }

        return $metadata;
    }

    /**
     * Check if class metadata is available in cache
     */
    public function hasMetadata(ClassName $className): bool
    {
        $cacheKey = $this->getCacheKeyObject($className);
        $results = $this->cache->has($cacheKey);

        return $results[$cacheKey->toString()] ?? false;
    }

    /**
     * Check if multiple classes have metadata in cache (batch operation)
     * @param ClassName[] $classNames
     * @return array<string, bool> Fully qualified class name to existence mapping
     */
    public function hasMetadataMultiple(array $classNames): array
    {
        $cacheKeys = [];
        $keyToClassName = [];

        foreach ($classNames as $className) {
            $cacheKey = $this->getCacheKeyObject($className);
            $cacheKeys[] = $cacheKey;
            $keyToClassName[$cacheKey->toString()] = $className->getFullyQualified();
        }

        $results = $this->cache->has(...$cacheKeys);

        // Map results back to class names
        $classResults = [];
        foreach ($results as $keyString => $exists) {
            $fullyQualified = $keyToClassName[$keyString];
            $classResults[$fullyQualified] = $exists;
        }

        return $classResults;
    }

    /**
     * Get specific metadata property without loading full ReflectionClass
     */
    public function getMetadataProperty(ClassName $className, string $property, mixed $default = null): mixed
    {
        $cacheKey = $this->getCacheKeyObject($className);
        $result = $this->cache->get($cacheKey);
        $cacheItem = $result->getItem($cacheKey);

        if (! $cacheItem->isHit) {
            return $default;
        }

        // Deserialize if needed (GeneralCache stores non-strings as serialized)
        $metadata = is_string($cacheItem->value) ? unserialize($cacheItem->value) : $cacheItem->value;

        if (! is_array($metadata)) {
            return $default;
        }

        return $metadata[$property] ?? $default;
    }

    /**
     * Get specific metadata properties for multiple classes (batch operation)
     * @param ClassName[] $classNames
     * @param string $property
     * @param mixed $default
     * @return array<string, mixed> Fully qualified class name to property value mapping
     */
    public function getMetadataPropertyMultiple(array $classNames, string $property, mixed $default = null): array
    {
        $cacheKeys = [];
        $keyToClassName = [];

        foreach ($classNames as $className) {
            $cacheKey = $this->getCacheKeyObject($className);
            $cacheKeys[] = $cacheKey;
            $keyToClassName[$cacheKey->toString()] = $className->getFullyQualified();
        }

        $result = $this->cache->get(...$cacheKeys);
        $propertyResults = [];

        foreach ($result->getItems() as $item) {
            $fullyQualified = $keyToClassName[$item->key->toString()];

            if (! $item->isHit) {
                $propertyResults[$fullyQualified] = $default;

                continue;
            }

            // Deserialize if needed
            $metadata = is_string($item->value) ? unserialize($item->value) : $item->value;

            if (! is_array($metadata)) {
                $propertyResults[$fullyQualified] = $default;

                continue;
            }

            $propertyResults[$fullyQualified] = $metadata[$property] ?? $default;
        }

        return $propertyResults;
    }

    /**
     * Remove cached metadata for a class
     */
    public function forget(ClassName $className): void
    {
        $cacheKey = $this->getCacheKeyObject($className);
        $this->cache->forget($cacheKey);
    }

    /**
     * Remove cached metadata for multiple classes (batch operation)
     * @param ClassName[] $classNames
     */
    public function forgetMultiple(array $classNames): void
    {
        $cacheKeys = array_map(fn ($className) => $this->getCacheKeyObject($className), $classNames);
        $this->cache->forget(...$cacheKeys);
    }

    /**
     * Get full metadata for multiple classes (batch operation)
     * @param ClassName[] $classNames
     * @param ReflectionClass[] $reflectionClasses Optional pre-loaded reflection classes
     * @return array<string, array> Fully qualified class name to metadata mapping
     */
    public function getMetadataMultiple(array $classNames, array $reflectionClasses = []): array
    {
        $cacheKeys = [];
        $keyToClassName = [];

        foreach ($classNames as $className) {
            $cacheKey = $this->getCacheKeyObject($className);
            $cacheKeys[] = $cacheKey;
            $keyToClassName[$cacheKey->toString()] = $className->getFullyQualified();
        }

        $result = $this->cache->get(...$cacheKeys);
        $metadataResults = [];
        $missingClasses = [];

        // Process cached results
        foreach ($result->getItems() as $item) {
            $fullyQualified = $keyToClassName[$item->key->toString()];

            if ($item->isHit) {
                // Deserialize if needed
                $metadata = is_string($item->value) ? unserialize($item->value) : $item->value;

                if (is_array($metadata)) {
                    $metadataResults[$fullyQualified] = $metadata;

                    continue;
                }
            }

            // Mark as missing for fallback extraction
            $missingClasses[$fullyQualified] = true;
        }

        // Extract metadata for missing classes if ReflectionClass objects provided
        foreach ($reflectionClasses as $reflectionClass) {
            $fullyQualified = $reflectionClass->getName();

            if (isset($missingClasses[$fullyQualified])) {
                $metadata = $this->extractMetadata($reflectionClass);
                $metadataResults[$fullyQualified] = $metadata;

                // Cache the extracted metadata for future use
                $className = ClassName::create($fullyQualified);
                $cacheKey = $this->getCacheKeyObject($className);
                $this->cache->set(CacheItem::forSet(
                    $cacheKey,
                    $metadata,
                    Duration::fromSeconds(self::MAX_CACHE_AGE)
                ));

                unset($missingClasses[$fullyQualified]);
            }
        }

        return $metadataResults;
    }

    /**
     * Clear all cached metadata
     */
    public function flush(): void
    {
        // Clear entire cache - this affects all cache entries, not just metadata
        // TODO: Implement prefix-based clearing in Cache interface
        $this->cache->clear();
    }

    /**
     * Get cache statistics
     */
    public function getStats(): Statistics
    {
        // Basic statistics - cache implementation specific details not accessible
        return Statistics::withMemory(
            counters: ['cached_classes' => 0], // Cannot determine without iteration
            memoryUsageMb: 0.0 // Cannot determine without cache driver stats
        )->withAdditionalMetadata([
            'cache_type' => get_class($this->cache),
            'max_age_hours' => self::MAX_CACHE_AGE / 3600,
            'cache_prefix' => self::CACHE_PREFIX,
        ]);
    }

    /**
     * Extract essential metadata from ReflectionClass
     *
     * @return array<string, mixed>
     */
    private function extractMetadata(ReflectionClass $class): array
    {
        return [
            '__version' => self::CACHE_VERSION,
            '__cached_at' => time(),
            '__class_name' => $class->getName(),

            // Essential class information
            'instantiable' => $class->isInstantiable(),
            'abstract' => $class->isAbstract(),
            'final' => $class->isFinal(),
            'interface' => $class->isInterface(),
            'trait' => $class->isTrait(),

            // Inheritance information
            'parent_class' => $class->getParentClass() ? $class->getParentClass()->getName() : null,
            'interfaces' => array_keys($class->getInterfaces()),
            'traits' => array_keys($class->getTraits()),

            // Basic metadata
            'short_name' => $class->getShortName(),
            'namespace' => $class->getNamespaceName(),
            'filename' => $class->getFileName(),

            // Method and property counts (for statistics)
            'method_count' => count($class->getMethods()),
            'property_count' => count($class->getProperties()),
            'constant_count' => count($class->getConstants()),
        ];
    }

    /**
     * Get cache key as CacheKey object (internal use)
     */
    private function getCacheKeyObject(ClassName $className): CacheKey
    {
        $hash = md5($className->getFullyQualified());

        return CacheKey::fromString(self::CACHE_PREFIX . $hash);
    }

    /**
     * Check if a class has metadata in the cache
     */
    public function has(ClassName $className, string $key): bool
    {
        $cacheKey = $this->getCacheKeyObject($className);
        $results = $this->cache->has($cacheKey);

        return $results[$cacheKey->toString()] ?? false;
    }

    /**
     * Generate a cache key for a class
     */
    public function getCacheKey(ClassName $className, string $suffix = ''): string
    {
        $hash = md5($className->getFullyQualified());
        $key = self::CACHE_PREFIX . $hash;

        return $suffix !== '' ? "{$key}::{$suffix}" : $key;
    }
}
