<?php

declare(strict_types=1);

namespace App\Framework\Reflection\Cache;

use App\Framework\Core\ValueObjects\ClassName;
use App\Framework\Core\ValueObjects\Statistics;
use App\Framework\Reflection\Collections\AttributeCollection;
use App\Framework\Reflection\Collections\MethodCollection;
use App\Framework\Reflection\Collections\PropertyCollection;
use App\Framework\Reflection\Contracts\ReflectionCache;
use App\Framework\Reflection\WrappedReflectionClass;
use ReflectionClass;

final class ClassCache implements ReflectionCache
{
    private const int MAX_CLASSES = 50;       // Extremely reduced to prevent memory exhaustion
    private const int MAX_PROPERTIES = 200;   // Extremely reduced to prevent memory exhaustion

    /**
     * @var array<string, ReflectionClass<object>>
     */
    private array $classCache = [];

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

    /**
     * @var array<string, bool>
     */
    private array $instantiableCache = [];

    /**
     * @var array<string, bool>
     */
    private array $interfaceCache = [];

    /**
     * @var array<string, WrappedReflectionClass>
     */
    private array $wrappedClassCache = [];

    /**
     * @var array<string, int> Access counter for LRU
     */
    private array $accessTimes = [];

    private int $accessCounter = 0;

    public function __construct(
        private readonly MethodCache $methodCache,
        private readonly AttributeCache $attributeCache,
        private readonly ?MetadataCacheManager $metadataCache = null
    ) {
    }

    /**
     * @return ReflectionClass<object>
     */
    public function getNativeClass(ClassName $className): ReflectionClass
    {
        $key = $className->getFullyQualified();

        // Record access for LRU
        $this->accessTimes[$key] = ++$this->accessCounter;

        if (isset($this->classCache[$key])) {
            return $this->classCache[$key];
        }

        // Check memory before creating new reflection class
        $memoryUsage = memory_get_usage();
        if ($memoryUsage > 300 * 1024 * 1024) { // Lowered to 300MB threshold
            // Aggressive cleanup when approaching memory limit
            $this->flush();

            // Also clear access times to prevent memory buildup
            $this->accessTimes = [];
            $this->accessCounter = 0;

            // Force garbage collection
            if (function_exists('gc_collect_cycles')) {
                gc_collect_cycles();
            }
        }

        /** @var class-string $key */
        $class = new ReflectionClass($key);
        $this->classCache[$key] = $class;

        // Store metadata for future fast access if MetadataCacheManager is available
        // Only cache for frequently accessed classes to prevent memory bloat
        if ($this->metadataCache !== null && $this->shouldCacheMetadata($className)) {
            $this->metadataCache->getMetadata($className, $class);
        }

        // Enforce memory limits more frequently
        if ($this->accessCounter % 50 === 0) { // Check every 50 accesses
            $this->enforceLimits();
        }

        return $class;
    }

    /**
     * Get a wrapped reflection class (legacy method name)
     * @deprecated Use getClass() instead
     */
    public function getWrappedClass(ClassName $className): WrappedReflectionClass
    {
        return $this->getClass($className);
    }

    /**
     * Get a wrapped reflection class
     */
    public function getClass(ClassName $className): WrappedReflectionClass
    {
        $key = $className->getFullyQualified();

        return $this->wrappedClassCache[$key] ??= new WrappedReflectionClass(
            $className,
            $this
        );
    }

    public function getShortName(ClassName $className): string
    {
        $key = "{$className->getFullyQualified()}::shortName";

        // Try metadata cache first for faster lookup
        if ($this->metadataCache !== null) {
            $cached = $this->metadataCache->getMetadataProperty($className, 'short_name');
            if ($cached !== null) {
                return $this->propertyCache[$key] = $cached;
            }
        }

        return $this->propertyCache[$key] ??= $this->getNativeClass($className)->getShortName();
    }

    public function getMethods(ClassName $className, ?int $filter = null): MethodCollection
    {
        return $this->methodCache->getMethods($className, $filter);
    }

    /**
     * @return array<\ReflectionMethod>
     */
    public function getNativeMethods(ClassName $className, ?int $filter = null): array
    {
        $key = "{$className->getFullyQualified()}::methods::" . ($filter ?? 'all');

        return $this->propertyCache[$key] ??= $this->getNativeClass($className)->getMethods($filter);
    }

    public function hasMethod(ClassName $className, string $methodName): bool
    {
        $key = "{$className->getFullyQualified()}::hasMethod::{$methodName}";

        return $this->propertyCache[$key] ??= $this->getNativeClass($className)->hasMethod($methodName);
    }

    public function getMethod(ClassName $className, string $methodName): \App\Framework\Reflection\WrappedReflectionMethod
    {
        return $this->methodCache->getMethod($className, $methodName);
    }

    public function getProperties(ClassName $className): PropertyCollection
    {
        $properties = $this->getNativeProperties($className);

        return new PropertyCollection(...$properties);
    }

    /**
     * @return array<\ReflectionProperty>
     */
    public function getNativeProperties(ClassName $className): array
    {
        $key = "{$className->getFullyQualified()}::properties";

        return $this->propertyCache[$key] ??= $this->getNativeClass($className)->getProperties();
    }

    public function getAttributes(ClassName $className, ?string $attributeClass = null): AttributeCollection
    {
        return $this->attributeCache->getClassAttributes($className, $attributeClass);
    }

    /**
     * @return array<\ReflectionAttribute<object>>
     */
    public function getNativeAttributes(ClassName $className, ?string $attributeClass = null): array
    {
        return $this->attributeCache->getNativeClassAttributes($className, $attributeClass);
    }

    public function hasAttribute(ClassName $className, string $attributeClass): bool
    {
        return ! $this->getAttributes($className, $attributeClass)->isEmpty();
    }

    public function isInstantiable(ClassName $className): bool
    {
        $key = $className->getFullyQualified();

        // Try metadata cache first for faster lookup
        if ($this->metadataCache !== null) {
            $cached = $this->metadataCache->getMetadataProperty($className, 'instantiable');
            if ($cached !== null) {
                return $this->instantiableCache[$key] = $cached;
            }
        }

        return $this->instantiableCache[$key] ??= $this->getNativeClass($className)->isInstantiable();
    }

    public function implementsInterface(ClassName $className, string $interfaceName): bool
    {
        $key = "{$className->getFullyQualified()}::implements::{$interfaceName}";

        // Try metadata cache first for faster lookup
        if ($this->metadataCache !== null) {
            $interfaces = $this->metadataCache->getMetadataProperty($className, 'interfaces', []);
            if (! empty($interfaces)) {
                return $this->interfaceCache[$key] = in_array($interfaceName, $interfaces, true);
            }
        }

        return $this->interfaceCache[$key] ??= $this->getNativeClass($className)->implementsInterface($interfaceName);
    }

    public function forget(ClassName $className): void
    {
        $key = $className->getFullyQualified();
        unset($this->classCache[$key]);
        unset($this->instantiableCache[$key]);
        unset($this->wrappedClassCache[$key]);

        // Clear all related caches
        $this->clearRelatedCaches($key);
    }

    public function flush(): void
    {
        $this->classCache = [];
        $this->propertyCache = [];
        $this->instantiableCache = [];
        $this->interfaceCache = [];
        $this->wrappedClassCache = [];
    }

    private function clearRelatedCaches(string $className): void
    {
        $prefix = $className . '::';

        // Clear propertyCache
        $filtered = [];
        foreach ($this->propertyCache as $key => $value) {
            if (! str_starts_with($key, $prefix)) {
                $filtered[$key] = $value;
            }
        }
        $this->propertyCache = $filtered;

        // Clear interfaceCache
        $filtered = [];
        foreach ($this->interfaceCache as $key => $value) {
            if (! str_starts_with($key, $prefix)) {
                $filtered[$key] = $value;
            }
        }
        $this->interfaceCache = $filtered;
    }

    public function getStats(): Statistics
    {
        return Statistics::countersOnly([
            'classes' => count($this->classCache),
            'properties' => count($this->propertyCache),
            'instantiable' => count($this->instantiableCache),
            'interfaces' => count($this->interfaceCache),
            'wrapped_classes' => count($this->wrappedClassCache),
        ]);
    }

    public function has(ClassName $className, string $key): bool
    {
        $cacheKey = $this->getCacheKey($className, $key);

        return match ($key) {
            'class' => isset($this->classCache[$cacheKey]),
            'properties' => isset($this->propertyCache[$cacheKey]),
            'instantiable' => isset($this->instantiableCache[$cacheKey]),
            'interface' => isset($this->interfaceCache[$cacheKey]),
            'wrapped' => isset($this->wrappedClassCache[$cacheKey]),
            default => false
        };
    }

    public function getCacheKey(ClassName $className, string $suffix = ''): string
    {
        $key = $className->getFullyQualified();

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

    /**
     * Enforce memory limits using LRU eviction
     */
    private function enforceLimits(): void
    {
        // Limit class cache
        if (count($this->classCache) > self::MAX_CLASSES) {
            $this->evictLRU($this->classCache, $this->wrappedClassCache);
        }

        // Limit property cache - more aggressive eviction
        if (count($this->propertyCache) > self::MAX_PROPERTIES) {
            $this->evictOldest($this->propertyCache, 150); // Remove most entries
        }

        // Also check interface cache and instantiable cache - much more aggressive
        if (count($this->interfaceCache) > 100) {
            $this->evictOldest($this->interfaceCache, 75);
        }

        if (count($this->instantiableCache) > 100) {
            $this->evictOldest($this->instantiableCache, 75);
        }
    }

    /**
     * @param array<string, mixed> $cache1
     * @param array<string, mixed> $cache2
     */
    private function evictLRU(array &$cache1, array &$cache2): void
    {
        // Find 100 least recently used items
        asort($this->accessTimes);
        $toRemove = array_slice(array_keys($this->accessTimes), 0, 100, true);

        foreach ($toRemove as $key) {
            unset(
                $cache1[$key],
                $cache2[$key],
                $this->accessTimes[$key],
                $this->instantiableCache[$key],
                $this->interfaceCache[$key]
            );
        }
    }

    /**
     * @param array<string, mixed> $cache
     */
    private function evictOldest(array &$cache, int $removeCount): void
    {
        $keys = array_slice(array_keys($cache), 0, $removeCount);
        foreach ($keys as $key) {
            unset($cache[$key]);
        }
    }

    /**
     * Determine if metadata should be cached for this class
     * Only cache framework core classes and frequently accessed classes
     */
    private function shouldCacheMetadata(ClassName $className): bool
    {
        $class = $className->getFullyQualified();

        // Cache framework core classes
        if (str_starts_with($class, 'App\\Framework\\')) {
            return true;
        }

        // Cache if this class has been accessed multiple times
        $key = $className->getFullyQualified();
        $accessCount = $this->accessTimes[$key] ?? 0;

        // Only cache after 2+ accesses to avoid one-time classes
        return $accessCount >= 2;
    }
}
