<?php

declare(strict_types=1);

namespace App\Framework\Reflection;

use App\Framework\Core\ValueObjects\ClassName;
use App\Framework\Reflection\Cache\MetadataCacheManager;
use ReflectionClass;

/**
 * Lazy-loading proxy for ReflectionClass instances
 *
 * Only creates the actual ReflectionClass when methods requiring it are called.
 * Uses metadata cache for faster access to commonly used reflection data.
 */
final class LazyReflectionProxy
{
    /**
     * The actual ReflectionClass instance, loaded on demand
     *
     * @var ReflectionClass<object>|null
     */
    private ?ReflectionClass $reflectionClass = null;

    /**
     * Cached metadata about the class
     *
     * @var array<string, mixed>|null
     */
    private ?array $metadata = null;

    /**
     * @param ClassName $className The class name to reflect
     * @param MetadataCacheManager $metadataCache The metadata cache manager
     */
    public function __construct(
        private readonly ClassName $className,
        private readonly MetadataCacheManager $metadataCache
    ) {
    }

    /**
     * Check if class is instantiable without loading ReflectionClass
     */
    public function isInstantiable(): bool
    {
        // Try metadata cache first
        $cached = $this->metadataCache->getMetadataProperty($this->className, 'instantiable');
        if ($cached !== null) {
            return $cached;
        }

        // Fallback to ReflectionClass
        return $this->getReflectionClass()->isInstantiable();
    }

    /**
     * Check if class implements interface without loading ReflectionClass
     */
    public function implementsInterface(string $interfaceName): bool
    {
        // Try metadata cache first
        $interfaces = $this->metadataCache->getMetadataProperty($this->className, 'interfaces', []);
        if (! empty($interfaces)) {
            return in_array($interfaceName, $interfaces, true);
        }

        // Fallback to ReflectionClass
        return $this->getReflectionClass()->implementsInterface($interfaceName);
    }

    /**
     * Get class short name without loading ReflectionClass
     */
    public function getShortName(): string
    {
        // Try metadata cache first
        $shortName = $this->metadataCache->getMetadataProperty($this->className, 'short_name');
        if ($shortName !== null) {
            return $shortName;
        }

        // Fallback to ReflectionClass
        return $this->getReflectionClass()->getShortName();
    }

    /**
     * Check if class is abstract without loading ReflectionClass
     */
    public function isAbstract(): bool
    {
        // Try metadata cache first
        $isAbstract = $this->metadataCache->getMetadataProperty($this->className, 'abstract');
        if ($isAbstract !== null) {
            return $isAbstract;
        }

        // Fallback to ReflectionClass
        return $this->getReflectionClass()->isAbstract();
    }

    /**
     * Check if class is final without loading ReflectionClass
     */
    public function isFinal(): bool
    {
        // Try metadata cache first
        $isFinal = $this->metadataCache->getMetadataProperty($this->className, 'final');
        if ($isFinal !== null) {
            return $isFinal;
        }

        // Fallback to ReflectionClass
        return $this->getReflectionClass()->isFinal();
    }

    /**
     * Get parent class name without loading ReflectionClass if possible
     *
     * Attempts to retrieve the parent class name from metadata cache first.
     * Falls back to using ReflectionClass if the information is not cached.
     *
     * @return string|null The fully qualified parent class name, or null if the class has no parent
     */
    public function getParentClass(): string|null
    {
        // Try metadata cache first
        $parentClass = $this->metadataCache->getMetadataProperty($this->className, 'parent_class');
        if ($parentClass !== null) {
            return $parentClass;
        }

        // Fallback to ReflectionClass
        $parent = $this->getReflectionClass()->getParentClass();

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

    /**
     * Get method count without loading full method reflection
     */
    public function getMethodCount(): int
    {
        // Try metadata cache first
        $methodCount = $this->metadataCache->getMetadataProperty($this->className, 'method_count');
        if ($methodCount !== null) {
            return $methodCount;
        }

        // Fallback to ReflectionClass
        return count($this->getReflectionClass()->getMethods());
    }

    /**
     * Get property count without loading full property reflection
     */
    public function getPropertyCount(): int
    {
        // Try metadata cache first
        $propertyCount = $this->metadataCache->getMetadataProperty($this->className, 'property_count');
        if ($propertyCount !== null) {
            return $propertyCount;
        }

        // Fallback to ReflectionClass
        return count($this->getReflectionClass()->getProperties());
    }

    /**
     * Get namespace without loading ReflectionClass
     */
    public function getNamespaceName(): string
    {
        // Try metadata cache first
        $namespace = $this->metadataCache->getMetadataProperty($this->className, 'namespace');
        if ($namespace !== null) {
            return $namespace;
        }

        // Fallback to ReflectionClass
        return $this->getReflectionClass()->getNamespaceName();
    }

    /**
     * Get the actual ReflectionClass instance
     *
     * This method creates the ReflectionClass instance if it hasn't been created yet.
     * It also stores metadata about the class in the cache for future use.
     * This is the core method that triggers lazy loading of the reflection data.
     *
     * @return ReflectionClass<object> The ReflectionClass instance for the class
     */
    public function getReflectionClass(): ReflectionClass
    {
        if ($this->reflectionClass === null) {
            /** @var class-string $className */
            $className = $this->className->getFullyQualified();
            $this->reflectionClass = new ReflectionClass($className);

            // Store metadata for future use
            $this->storeMetadata();
        }

        return $this->reflectionClass;
    }

    /**
     * Get class name
     */
    public function getName(): string
    {
        return $this->className->getFullyQualified();
    }

    /**
     * Check if ReflectionClass has been loaded
     *
     * @return bool True if the ReflectionClass has been loaded, false otherwise
     */
    public function isLoaded(): bool
    {
        return $this->reflectionClass !== null;
    }

    /**
     * Check if the class is an enum
     *
     * Attempts to determine if the class is an enum without loading the full ReflectionClass.
     * Falls back to using ReflectionClass if the information is not cached.
     *
     * @return bool True if the class is an enum, false otherwise
     */
    public function isEnum(): bool
    {
        // Try metadata cache first
        $isEnum = $this->metadataCache->getMetadataProperty($this->className, 'is_enum');
        if ($isEnum !== null) {
            return $isEnum;
        }

        // Fallback to ReflectionClass
        return $this->getReflectionClass()->isEnum();
    }

    /**
     * Get enum cases for an enum class
     *
     * Returns the enum cases if the class is an enum.
     * For non-enum classes, returns an empty array.
     *
     * @return array<string, object> Array of enum cases with case names as keys
     */
    public function getEnumCases(): array
    {
        // If not an enum, return empty array
        if (! $this->isEnum()) {
            return [];
        }

        // Try metadata cache first
        $cases = $this->metadataCache->getMetadataProperty($this->className, 'enum_cases');
        if ($cases !== null) {
            return $cases;
        }

        // Fallback to ReflectionClass
        $reflectionCases = $this->getReflectionClass()->getCases();
        $cases = [];

        foreach ($reflectionCases as $case) {
            $caseName = $case->getName();
            /** @var class-string $className */
            $className = $this->className->getFullyQualified();
            $cases[$caseName] = $className::$caseName;
        }

        return $cases;
    }

    /**
     * Check if the class has a specific attribute
     *
     * Attempts to determine if the class has a specific attribute without loading
     * the full ReflectionClass. Falls back to using ReflectionClass if the information
     * is not cached.
     *
     * @param string $attributeName The fully qualified attribute class name
     * @param int $flags Optional flags for attribute lookup (ReflectionAttribute::IS_INSTANCEOF)
     * @return bool True if the class has the attribute, false otherwise
     */
    public function hasAttribute(string $attributeName, int $flags = 0): bool
    {
        // Try metadata cache first
        $attributes = $this->metadataCache->getMetadataProperty($this->className, 'attributes');
        if ($attributes !== null) {
            // Simple check if we're not using IS_INSTANCEOF flag
            if ($flags === 0) {
                return isset($attributes[$attributeName]);
            }

            // More complex check for IS_INSTANCEOF
            if ($flags & 2) { // ReflectionAttribute::IS_INSTANCEOF = 2
                foreach (array_keys($attributes) as $attrClass) {
                    if (is_a($attrClass, $attributeName, true)) {
                        return true;
                    }
                }

                return false;
            }
        }

        // Fallback to ReflectionClass
        return count($this->getReflectionClass()->getAttributes($attributeName, $flags)) > 0;
    }

    /**
     * Get attributes of the class
     *
     * Returns the attributes of the class.
     *
     * @param string|null $attributeName Optional attribute class name to filter by
     * @param int $flags Optional flags for attribute lookup (ReflectionAttribute::IS_INSTANCEOF)
     * @return array<\ReflectionAttribute<object>> Array of ReflectionAttribute objects
     */
    public function getAttributes(?string $attributeName = null, int $flags = 0): array
    {
        // Try metadata cache first for the existence check
        if ($attributeName !== null) {
            $hasAttribute = $this->hasAttribute($attributeName, $flags);
            if (! $hasAttribute) {
                return [];
            }
        }

        // Fallback to ReflectionClass for the actual attributes
        return $this->getReflectionClass()->getAttributes($attributeName, $flags);
    }

    /**
     * Get attribute instances of the class (PHP 8.0+)
     *
     * Returns the instantiated attribute objects of the class. For PHP versions below 8.0,
     * returns an empty array.
     *
     * @param string|null $attributeName Optional attribute class name to filter by
     * @param int $flags Optional flags for attribute lookup (ReflectionAttribute::IS_INSTANCEOF)
     * @return array<object> Array of instantiated attribute objects
     */
    public function getAttributeInstances(?string $attributeName = null, int $flags = 0): array
    {
        $attributes = $this->getAttributes($attributeName, $flags);
        $instances = [];

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

        return $instances;
    }

    /**
     * Get memory usage statistics for this proxy
     *
     * Provides information about the memory usage of this proxy, including:
     * - The class name being reflected
     * - Whether the ReflectionClass has been loaded
     * - Whether metadata is cached
     * - An estimated memory usage in bytes
     *
     * @return array{
     *     class_name: string,
     *     reflection_loaded: bool,
     *     has_cached_metadata: bool,
     *     estimated_memory_bytes: int
     * } Memory usage statistics
     */
    public function getMemoryStats(): array
    {
        return [
            'class_name' => $this->className->getFullyQualified(),
            'reflection_loaded' => $this->reflectionClass !== null,
            'has_cached_metadata' => $this->metadataCache->hasMetadata($this->className),
            'estimated_memory_bytes' => $this->reflectionClass !== null ? 8192 : 512, // Rough estimate
        ];
    }

    /**
     * Store metadata in cache after loading ReflectionClass
     *
     * This method extracts and stores metadata about the class in the cache,
     * including information about attributes and enum cases (PHP 8.0+ and 8.1+).
     */
    private function storeMetadata(): void
    {
        if ($this->reflectionClass === null || $this->metadataCache->hasMetadata($this->className)) {
            return;
        }

        // Extract basic metadata
        $metadata = [
            'short_name' => $this->reflectionClass->getShortName(),
            'namespace' => $this->reflectionClass->getNamespaceName(),
            'parent_class' => ($parent = $this->reflectionClass->getParentClass()) ? $parent->getName() : null,
            'instantiable' => $this->reflectionClass->isInstantiable(),
            'abstract' => $this->reflectionClass->isAbstract(),
            'final' => $this->reflectionClass->isFinal(),
            'method_count' => count($this->reflectionClass->getMethods()),
            'property_count' => count($this->reflectionClass->getProperties()),
        ];

        // Extract interfaces
        $interfaces = $this->reflectionClass->getInterfaceNames();
        $metadata['interfaces'] = $interfaces;

        // Extract attribute information
        $attributes = [];
        $reflectionAttributes = $this->reflectionClass->getAttributes();

        foreach ($reflectionAttributes as $attribute) {
            $attributeName = $attribute->getName();

            try {
                $attributes[$attributeName] = $attribute->getArguments();
            } catch (\Throwable $e) {
                // Skip attributes that can't be processed
                continue;
            }
        }

        $metadata['attributes'] = $attributes;

        // Extract enum information
        $metadata['is_enum'] = $this->reflectionClass->isEnum();

        // If it's an enum, extract cases
        if ($metadata['is_enum']) {
            $cases = [];
            $reflectionCases = $this->reflectionClass->getCases();

            foreach ($reflectionCases as $case) {
                $caseName = $case->getName();
                /** @var class-string $className */
                $className = $this->className->getFullyQualified();
                $cases[$caseName] = $className::$caseName;
            }

            $metadata['enum_cases'] = $cases;
        }

        // Store the metadata
        $this->metadataCache->storeMetadata($this->className, $metadata);
    }
}
