<?php

declare(strict_types=1);

namespace App\Framework\Reflection;

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\ParameterCollection;
use App\Framework\Reflection\Collections\PropertyCollection;
use App\Framework\Reflection\Factory\ReflectionCacheFactory;
use ReflectionClass;
use ReflectionMethod;
use ReflectionParameter;
use ReflectionProperty;
use ReflectionType;

final readonly class CachedReflectionProvider implements ReflectionProvider
{
    private ReflectionCache $cache;

    /**
     * @param ReflectionCache|null $cache The reflection cache to use, or null to create one
     * @param ReflectionCacheFactory|null $factory The factory to use for creating a cache if none is provided
     * @param string|null $environment The environment to use for cache configuration ('production' or 'development')
     */
    public function __construct(
        ?ReflectionCache $cache = null,
        ?ReflectionCacheFactory $factory = null,
        ?string $environment = null
    ) {
        if ($cache === null) {
            $factory ??= new ReflectionCacheFactory();
            $env = $environment ?? 'production';

            // Use optimized cache in production, regular cache in development
            $this->cache = $env === 'development'
                ? $factory->createForDevelopment()
                : $factory->createOptimized();
        } else {
            $this->cache = $cache;
        }
    }

    // ClassReflector implementation
    public function getClass(ClassName $className): WrappedReflectionClass
    {
        return $this->cache->classCache->getClass($className);
    }

    public function getNativeClass(ClassName $className): ReflectionClass
    {
        return $this->cache->classCache->getNativeClass($className);
    }

    public function getProperties(ClassName $className): PropertyCollection
    {
        return $this->cache->classCache->getProperties($className);
    }

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

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

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

    public function isInstantiable(ClassName $className): bool
    {
        return $this->cache->classCache->isInstantiable($className);
    }

    public function implementsInterface(ClassName $className, string $interfaceName): bool
    {
        return $this->cache->classCache->implementsInterface($className, $interfaceName);
    }

    // MethodReflector implementation
    public function getMethod(ClassName $className, string $methodName): WrappedReflectionMethod
    {
        return $this->cache->methodCache->getMethod($className, $methodName);
    }

    public function getNativeMethod(ClassName $className, string $methodName): ReflectionMethod
    {
        return $this->cache->methodCache->getNativeMethod($className, $methodName);
    }

    public function getMethodParameters(ClassName $className, string $methodName): ParameterCollection
    {
        return $this->cache->parameterCache->getParameters($className, $methodName);
    }

    public function getMethodAttributes(ClassName $className, string $methodName, ?string $attributeClass = null): AttributeCollection
    {
        return $this->cache->methodCache->getMethodAttributes($className, $methodName, $attributeClass);
    }

    public function getParameterInfo(ClassName $className, string $methodName): array
    {
        return $this->cache->parameterCache->getParameterInfo($className, $methodName);
    }

    // PropertyReflector implementation
    public function getProperty(ClassName $className, string $propertyName): ReflectionProperty
    {
        return $this->getNativeClass($className)->getProperty($propertyName);
    }

    public function hasProperty(ClassName $className, string $propertyName): bool
    {
        return $this->getNativeClass($className)->hasProperty($propertyName);
    }

    public function getPropertiesWithAttribute(ClassName $className, string $attributeClass): PropertyCollection
    {
        $properties = $this->getProperties($className);
        $result = [];

        foreach ($properties as $property) {
            if (count($property->getAttributes($attributeClass)) > 0) {
                $result[] = $property;
            }
        }

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

    public function getPropertyAttributes(ClassName $className, string $propertyName, ?string $attributeClass = null): array
    {
        $property = $this->getProperty($className, $propertyName);

        return $property->getAttributes($attributeClass);
    }

    public function getPropertyType(ClassName $className, string $propertyName): ?ReflectionType
    {
        $property = $this->getProperty($className, $propertyName);

        return $property->getType();
    }

    public function getPropertyDefaultValue(ClassName $className, string $propertyName): mixed
    {
        $property = $this->getProperty($className, $propertyName);

        if (! $property->hasDefaultValue()) {
            return null;
        }

        return $property->getDefaultValue();
    }

    // ParameterReflector implementation
    public function getMethodParameter(ClassName $className, string $methodName, string $parameterName): ReflectionParameter
    {
        $method = $this->getNativeMethod($className, $methodName);

        foreach ($method->getParameters() as $parameter) {
            if ($parameter->getName() === $parameterName) {
                return $parameter;
            }
        }

        throw new \ReflectionException("Parameter {$parameterName} does not exist in method {$methodName}");
    }

    public function hasMethodParameter(ClassName $className, string $methodName, string $parameterName): bool
    {
        $method = $this->getNativeMethod($className, $methodName);

        foreach ($method->getParameters() as $parameter) {
            if ($parameter->getName() === $parameterName) {
                return true;
            }
        }

        return false;
    }

    public function getParameterType(ClassName $className, string $methodName, string $parameterName): ?ReflectionType
    {
        $parameter = $this->getMethodParameter($className, $methodName, $parameterName);

        return $parameter->getType();
    }

    public function getParameterDefaultValue(ClassName $className, string $methodName, string $parameterName): mixed
    {
        $parameter = $this->getMethodParameter($className, $methodName, $parameterName);

        if (! $parameter->isDefaultValueAvailable()) {
            return null;
        }

        return $parameter->getDefaultValue();
    }

    public function isParameterOptional(ClassName $className, string $methodName, string $parameterName): bool
    {
        $parameter = $this->getMethodParameter($className, $methodName, $parameterName);

        return $parameter->isOptional();
    }

    public function isParameterVariadic(ClassName $className, string $methodName, string $parameterName): bool
    {
        $parameter = $this->getMethodParameter($className, $methodName, $parameterName);

        return $parameter->isVariadic();
    }

    public function getParameterAttributes(
        ClassName $className,
        string $methodName,
        string $parameterName,
        ?string $attributeClass = null
    ): array {
        $parameter = $this->getMethodParameter($className, $methodName, $parameterName);

        return $parameter->getAttributes($attributeClass);
    }

    // AttributeReflector implementation
    public function hasMethodAttribute(ClassName $className, string $methodName, string $attributeClass): bool
    {
        $attributes = $this->getMethodAttributes($className, $methodName, $attributeClass);

        return ! $attributes->isEmpty();
    }

    public function getAttributeInstances(ClassName $className, ?string $attributeClass = null): array
    {
        $attributes = $this->getAttributes($className, $attributeClass);
        $instances = [];

        foreach ($attributes as $attribute) {
            try {
                $instances[] = $attribute->newInstance();
            } catch (\Throwable) {
                // Skip attributes that can't be instantiated
            }
        }

        return $instances;
    }

    public function getMethodAttributeInstances(ClassName $className, string $methodName, ?string $attributeClass = null): array
    {
        $attributes = $this->getMethodAttributes($className, $methodName, $attributeClass);
        $instances = [];

        foreach ($attributes as $attribute) {
            try {
                $instances[] = $attribute->newInstance();
            } catch (\Throwable) {
                // Skip attributes that can't be instantiated
            }
        }

        return $instances;
    }

    // EnumReflector implementation
    public function isEnum(ClassName $className): bool
    {
        return $this->getNativeClass($className)->isEnum();
    }

    public function getEnumCases(ClassName $className): array
    {
        if (! $this->isEnum($className)) {
            throw new \ReflectionException("Class {$className->getFullyQualified()} is not an enum");
        }

        // Use ReflectionEnum instead of ReflectionClass for enum-specific methods
        /** @var class-string $classNameString */
        $classNameString = $className->getFullyQualified();
        $reflectionEnum = new \ReflectionEnum($classNameString);
        $reflectionCases = $reflectionEnum->getCases();

        $cases = [];
        foreach ($reflectionCases as $case) {
            $caseName = $case->getName();
            // Use constant() function to dynamically access enum case
            $cases[$caseName] = constant("$classNameString::$caseName");
        }

        return $cases;
    }

    public function getEnumCase(ClassName $className, string $caseName): object
    {
        if (! $this->isEnum($className)) {
            throw new \ReflectionException("Class {$className->getFullyQualified()} is not an enum");
        }

        if (! $this->hasEnumCase($className, $caseName)) {
            throw new \ReflectionException("Enum case {$caseName} does not exist in enum {$className->getFullyQualified()}");
        }

        /** @var class-string $classNameString */
        $classNameString = $className->getFullyQualified();

        // Use constant() function to dynamically access enum case
        return constant("$classNameString::$caseName");
    }

    public function hasEnumCase(ClassName $className, string $caseName): bool
    {
        if (! $this->isEnum($className)) {
            return false;
        }

        // Use ReflectionEnum instead of ReflectionClass for enum-specific methods
        /** @var class-string $classNameString */
        $classNameString = $className->getFullyQualified();
        $reflectionEnum = new \ReflectionEnum($classNameString);

        // ReflectionEnum has a hasCase method we can use directly
        return $reflectionEnum->hasCase($caseName);
    }

    public function getEnumCaseAttributes(
        ClassName $className,
        string $caseName,
        ?string $attributeClass = null
    ): array {
        if (! $this->isEnum($className)) {
            throw new \ReflectionException("Class {$className->getFullyQualified()} is not an enum");
        }

        // Use ReflectionEnum instead of ReflectionClass for enum-specific methods
        /** @var class-string $classNameString */
        $classNameString = $className->getFullyQualified();
        $reflectionEnum = new \ReflectionEnum($classNameString);

        // Check if the case exists
        if (! $reflectionEnum->hasCase($caseName)) {
            throw new \ReflectionException("Enum case {$caseName} does not exist in enum {$className->getFullyQualified()}");
        }

        // Get the case directly using getCase method
        $case = $reflectionEnum->getCase($caseName);

        return $case->getAttributes($attributeClass);
    }

    public function isBackedEnum(ClassName $className): bool
    {
        if (! $this->isEnum($className)) {
            return false;
        }

        // Use ReflectionEnum instead of ReflectionClass for enum-specific methods
        /** @var class-string $classNameString */
        $classNameString = $className->getFullyQualified();
        $reflectionEnum = new \ReflectionEnum($classNameString);

        return $reflectionEnum->isBacked();
    }

    public function getEnumBackingType(ClassName $className): ?string
    {
        if (! $this->isBackedEnum($className)) {
            return null;
        }

        // Use ReflectionEnum instead of ReflectionClass for enum-specific methods
        /** @var class-string $classNameString */
        $classNameString = $className->getFullyQualified();
        $reflectionEnum = new \ReflectionEnum($classNameString);
        $backingType = $reflectionEnum->getBackingType();

        return $backingType ? $backingType->getName() : null;
    }

    public function getEnumCaseBackingValue(ClassName $className, string $caseName): int|string|null
    {
        if (! $this->isBackedEnum($className)) {
            return null;
        }

        $case = $this->getEnumCase($className, $caseName);

        // For backed enums, the value property contains the backing value
        return $case->value;
    }

    // InstantiationReflector implementation
    public function createInstance(ClassName $className, array $args = []): object
    {
        if (! $this->isInstantiable($className)) {
            throw new \ReflectionException("Class {$className->getFullyQualified()} is not instantiable");
        }

        $class = $this->getNativeClass($className);

        return $class->newInstance(...$args);
    }

    public function createInstanceWithoutConstructor(ClassName $className): object
    {
        $class = $this->getNativeClass($className);

        return $class->newInstanceWithoutConstructor();
    }

    public function createLazyGhost(ClassName $className, callable $initializer, int $options = 0): object
    {
        $class = $this->getNativeClass($className);

        return $class->newLazyGhost($initializer, $options);
    }

    public function createLazyProxy(ClassName $className, callable $factory, int $options = 0): object
    {
        $class = $this->getNativeClass($className);

        return $class->newLazyProxy($factory, $options);
    }

    public function hasConstructor(ClassName $className): bool
    {
        $class = $this->getNativeClass($className);

        return $class->getConstructor() !== null;
    }

    public function getConstructorParameters(ClassName $className): array
    {
        $class = $this->getNativeClass($className);
        $constructor = $class->getConstructor();

        if ($constructor === null) {
            return [];
        }

        return $constructor->getParameters();
    }

    public function isAbstract(ClassName $className): bool
    {
        return $this->getNativeClass($className)->isAbstract();
    }

    public function isFinal(ClassName $className): bool
    {
        return $this->getNativeClass($className)->isFinal();
    }

    public function isInternal(ClassName $className): bool
    {
        return $this->getNativeClass($className)->isInternal();
    }

    public function isUserDefined(ClassName $className): bool
    {
        return $this->getNativeClass($className)->isUserDefined();
    }

    // CacheManager implementation
    public function forget(ClassName $className): void
    {
        $this->cache->forget($className);
    }

    public function flush(): void
    {
        $this->cache->flush();
    }

    public function getStats(): Statistics
    {
        return $this->cache->getStats();
    }

    /**
     * Get the underlying cache instance
     */
    public function getCache(): ReflectionCache
    {
        return $this->cache;
    }
}
