<?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\WrappedReflectionAttribute;

final class AttributeCache implements \App\Framework\Reflection\Contracts\ReflectionCache
{
    /**
     * @var array<string, mixed>
     */
    private array $attributeCache = [];

    public function getClassAttributes(ClassName $className, ?string $attributeClass = null): AttributeCollection
    {
        $nativeAttributes = $this->getNativeClassAttributes($className, $attributeClass);
        $wrappedAttributes = [];

        foreach ($nativeAttributes as $index => $attribute) {
            // BUGFIX: Skip attributes with empty names (failed to load)
            $attributeName = $attribute->getName();
            if (empty($attributeName)) {
                continue;
            }

            $wrappedAttributes[] = new WrappedReflectionAttribute(
                $className,
                null, // no method for class attributes
                $attributeName,
                $index,
                $this
            );
        }

        return new AttributeCollection(...$wrappedAttributes);
    }

    /**
     * @return array<\ReflectionAttribute<object>>
     */
    public function getNativeClassAttributes(ClassName $className, ?string $attributeClass = null): array
    {
        $key = "{$className->getFullyQualified()}::attributes::" . ($attributeClass ?? 'all');
        if (! isset($this->attributeCache[$key])) {
            /** @var class-string $className */
            $className = $className->getFullyQualified();
            $class = new \ReflectionClass($className);
            $this->attributeCache[$key] = $attributeClass
                ? $class->getAttributes($attributeClass)
                : $class->getAttributes();
        }

        return $this->attributeCache[$key];
    }

    public function getMethodAttributes(ClassName $className, string $methodName, ?string $attributeClass = null): AttributeCollection
    {
        $nativeAttributes = $this->getNativeMethodAttributes($className, $methodName, $attributeClass);
        $wrappedAttributes = [];

        foreach ($nativeAttributes as $index => $attribute) {
            // BUGFIX: Skip attributes with empty names (failed to load)
            $attributeName = $attribute->getName();
            if (empty($attributeName)) {
                continue;
            }

            $wrappedAttributes[] = new WrappedReflectionAttribute(
                $className,
                $methodName,
                $attributeName,
                $index,
                $this
            );
        }

        return new AttributeCollection(...$wrappedAttributes);
    }

    /**
     * @return array<\ReflectionAttribute<object>>
     */
    public function getNativeMethodAttributes(ClassName $className, string $methodName, ?string $attributeClass = null): array
    {
        $key = "{$className->getFullyQualified()}::{$methodName}::attributes::" . ($attributeClass ?? 'all');
        if (! isset($this->attributeCache[$key])) {
            /** @var class-string $classNameString */
            $classNameString = $className->getFullyQualified();
            $class = new \ReflectionClass($classNameString);
            $method = $class->getMethod($methodName);
            $this->attributeCache[$key] = $attributeClass
                ? $method->getAttributes($attributeClass)
                : $method->getAttributes();
        }

        return $this->attributeCache[$key];
    }

    // Attribute-specific cache methods for WrappedReflectionAttribute
    public function getAttributeName(ClassName $className, ?string $methodName, string $attributeClass, int $index): string
    {
        $key = $this->buildAttributeKey($className, $methodName, $attributeClass, $index, 'name');
        if (! isset($this->attributeCache[$key])) {
            // BUGFIX: Use ALL attributes (null filter) to match the indexing from getMethodAttributes
            $attributes = $this->getNativeAttributesForContext($className, $methodName, null);
            $this->attributeCache[$key] = isset($attributes[$index]) ? $attributes[$index]->getName() : '';
        }

        return $this->attributeCache[$key];
    }

    public function getAttributeShortName(ClassName $className, ?string $methodName, string $attributeClass, int $index): string
    {
        $name = $this->getAttributeName($className, $methodName, $attributeClass, $index);
        $parts = explode('\\', $name);

        return end($parts);
    }

    public function getAttributeTarget(ClassName $className, ?string $methodName, string $attributeClass, int $index): int
    {
        $key = $this->buildAttributeKey($className, $methodName, $attributeClass, $index, 'target');
        if (! isset($this->attributeCache[$key])) {
            // BUGFIX: Use ALL attributes (null filter) to match the indexing from getMethodAttributes
            $attributes = $this->getNativeAttributesForContext($className, $methodName, null);
            $this->attributeCache[$key] = isset($attributes[$index]) ? $attributes[$index]->getTarget() : 0;
        }

        return $this->attributeCache[$key];
    }

    public function isAttributeRepeated(ClassName $className, ?string $methodName, string $attributeClass, int $index): bool
    {
        $key = $this->buildAttributeKey($className, $methodName, $attributeClass, $index, 'repeated');
        if (! isset($this->attributeCache[$key])) {
            // BUGFIX: Use ALL attributes (null filter) to match the indexing from getMethodAttributes
            $attributes = $this->getNativeAttributesForContext($className, $methodName, null);
            $this->attributeCache[$key] = isset($attributes[$index]) ? $attributes[$index]->isRepeated() : false;
        }

        return $this->attributeCache[$key];
    }

    /**
     * @return array<mixed>
     */
    public function getAttributeArguments(ClassName $className, ?string $methodName, string $attributeClass, int $index): array
    {
        $key = $this->buildAttributeKey($className, $methodName, $attributeClass, $index, 'arguments');
        if (! isset($this->attributeCache[$key])) {
            // BUGFIX: Use ALL attributes (null filter) to match the indexing from getMethodAttributes
            $attributes = $this->getNativeAttributesForContext($className, $methodName, null);
            $this->attributeCache[$key] = isset($attributes[$index]) ? $attributes[$index]->getArguments() : [];
        }

        return $this->attributeCache[$key];
    }

    public function getAttributeInstance(ClassName $className, ?string $methodName, string $attributeClass, int $index): ?object
    {
        $key = $this->buildAttributeKey($className, $methodName, $attributeClass, $index, 'instance');
        if (! isset($this->attributeCache[$key])) {
            // BUGFIX: Use ALL attributes (null filter) to match the indexing from getMethodAttributes
            $attributes = $this->getNativeAttributesForContext($className, $methodName, null);
            $this->attributeCache[$key] = isset($attributes[$index]) ? $attributes[$index]->newInstance() : null;
        }

        return $this->attributeCache[$key];
    }

    private function buildAttributeKey(ClassName $className, ?string $methodName, string $attributeClass, int $index, string $property): string
    {
        $context = $methodName ? "{$className->getFullyQualified()}::{$methodName}" : $className->getFullyQualified();

        return "{$context}::attr::{$attributeClass}::{$index}::{$property}";
    }

    /**
     * @return array<\ReflectionAttribute<object>>
     */
    private function getNativeAttributesForContext(ClassName $className, ?string $methodName, ?string $attributeClass): array
    {
        if ($methodName !== null) {
            return $this->getNativeMethodAttributes($className, $methodName, $attributeClass);
        }

        return $this->getNativeClassAttributes($className, $attributeClass);
    }

    public function forget(ClassName $className): void
    {
        $this->clearRelatedCaches($className->getFullyQualified());
    }

    public function flush(): void
    {
        $this->attributeCache = [];
    }

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

        // Clear attributeCache
        $filtered = array_filter($this->attributeCache, function ($key) use ($prefix) {
            return ! str_starts_with($key, $prefix);
        }, ARRAY_FILTER_USE_KEY);
        $this->attributeCache = $filtered;
    }

    public function getStats(): Statistics
    {
        return Statistics::countersOnly([
            'attributes' => count($this->attributeCache),
        ]);
    }

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

        return isset($this->attributeCache[$cacheKey]);
    }

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

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