<?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\ParameterCollection;
use App\Framework\Reflection\WrappedReflectionParameter;
use InvalidArgumentException;

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

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

    /**
     * @var array<string, WrappedReflectionParameter>
     */
    private array $wrappedParameterCache = [];

    public function getParameters(ClassName $className, string $methodName): ParameterCollection
    {
        $parameters = [];
        $nativeParams = $this->getNativeParameters($className, $methodName);

        foreach ($nativeParams as $position => $param) {
            $parameters[] = $this->getWrappedParameter($className, $methodName, $position);
        }

        return new ParameterCollection(...$parameters);
    }

    /**
     * @return array<\ReflectionParameter>
     */
    public function getNativeParameters(ClassName $className, string $methodName): array
    {
        $key = "{$className->getFullyQualified()}::{$methodName}::params";
        if (! isset($this->parameterCache[$key])) {
            /** @var class-string $classNameString */
            $classNameString = $className->getFullyQualified();
            $class = new \ReflectionClass($classNameString);
            $method = $class->getMethod($methodName);
            $this->parameterCache[$key] = $method->getParameters();
        }

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

    public function getWrappedParameter(ClassName $className, string $methodName, int $position): WrappedReflectionParameter
    {
        $key = "{$className->getFullyQualified()}::{$methodName}::param::{$position}";
        if (! isset($this->wrappedParameterCache[$key])) {
            // Verify parameter exists
            $params = $this->getNativeParameters($className, $methodName);
            if (! isset($params[$position])) {
                throw new InvalidArgumentException("Parameter at position {$position} not found");
            }
            $this->wrappedParameterCache[$key] = new WrappedReflectionParameter(
                $className,
                $methodName,
                $position,
                $this
            );
        }

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

    /**
     * @return array<array<string, mixed>>
     */
    public function getParameterInfo(ClassName $className, string $methodName): array
    {
        $key = "{$className->getFullyQualified()}::{$methodName}::paramInfo";

        return $this->parameterInfoCache[$key] ??= $this->buildParameterInfo($className, $methodName);
    }

    // Individual parameter property methods
    public function getParameterType(ClassName $className, string $methodName, int $position): ?\ReflectionType
    {
        $key = "{$className->getFullyQualified()}::{$methodName}::param::{$position}::type";
        if (! isset($this->parameterCache[$key])) {
            $params = $this->getNativeParameters($className, $methodName);
            $this->parameterCache[$key] = isset($params[$position]) ? $params[$position]->getType() : null;
        }

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

    public function getParameterTypeName(ClassName $className, string $methodName, int $position): ?string
    {
        $type = $this->getParameterType($className, $methodName, $position);
        if ($type === null) {
            return null;
        }

        // ReflectionNamedType has getName(), ReflectionUnionType/IntersectionType don't
        if ($type instanceof \ReflectionNamedType) {
            return $type->getName();
        }

        // For union/intersection types, return string representation
        return (string) $type;
    }

    public function isParameterTypeBuiltin(ClassName $className, string $methodName, int $position): bool
    {
        $type = $this->getParameterType($className, $methodName, $position);
        if ($type === null) {
            return true; // No type = mixed = builtin
        }

        // Only ReflectionNamedType has isBuiltin()
        if ($type instanceof \ReflectionNamedType) {
            return $type->isBuiltin();
        }

        // Union/intersection types are not builtin
        return false;
    }

    public function isParameterOptional(ClassName $className, string $methodName, int $position): bool
    {
        $key = "{$className->getFullyQualified()}::{$methodName}::param::{$position}::optional";
        if (! isset($this->parameterCache[$key])) {
            $params = $this->getNativeParameters($className, $methodName);
            $this->parameterCache[$key] = isset($params[$position]) ? $params[$position]->isOptional() : false;
        }

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

    public function parameterHasDefaultValue(ClassName $className, string $methodName, int $position): bool
    {
        $key = "{$className->getFullyQualified()}::{$methodName}::param::{$position}::hasDefault";
        if (! isset($this->parameterCache[$key])) {
            $params = $this->getNativeParameters($className, $methodName);
            $this->parameterCache[$key] = isset($params[$position]) ? $params[$position]->isDefaultValueAvailable() : false;
        }

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

    public function getParameterDefaultValue(ClassName $className, string $methodName, int $position): mixed
    {
        $key = "{$className->getFullyQualified()}::{$methodName}::param::{$position}::default";
        if (! isset($this->parameterCache[$key])) {
            $params = $this->getNativeParameters($className, $methodName);
            $this->parameterCache[$key] = isset($params[$position]) && $params[$position]->isDefaultValueAvailable()
                ? $params[$position]->getDefaultValue()
                : null;
        }

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

    public function parameterAllowsNull(ClassName $className, string $methodName, int $position): bool
    {
        $key = "{$className->getFullyQualified()}::{$methodName}::param::{$position}::allowsNull";
        if (! isset($this->parameterCache[$key])) {
            $params = $this->getNativeParameters($className, $methodName);
            $this->parameterCache[$key] = isset($params[$position]) ? $params[$position]->allowsNull() : false;
        }

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

    public function getParameterPosition(ClassName $className, string $methodName, int $position): int
    {
        $key = "{$className->getFullyQualified()}::{$methodName}::param::{$position}::position";
        if (! isset($this->parameterCache[$key])) {
            $params = $this->getNativeParameters($className, $methodName);
            $this->parameterCache[$key] = isset($params[$position]) ? $params[$position]->getPosition() : $position;
        }

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

    public function getParameterName(ClassName $className, string $methodName, int $position): string
    {
        $key = "{$className->getFullyQualified()}::{$methodName}::param::{$position}::name";
        if (! isset($this->parameterCache[$key])) {
            $params = $this->getNativeParameters($className, $methodName);
            $this->parameterCache[$key] = isset($params[$position]) ? $params[$position]->getName() : '';
        }

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

    /**
     * @return array<array<string, mixed>>
     */
    private function buildParameterInfo(ClassName $className, string $methodName): array
    {
        $parameters = $this->getNativeParameters($className, $methodName);
        $paramInfo = [];

        foreach ($parameters as $param) {
            $type = $param->getType();
            $typeName = null;
            $isBuiltin = true;

            if ($type instanceof \ReflectionNamedType) {
                $typeName = $type->getName();
                $isBuiltin = $type->isBuiltin();
            } elseif ($type instanceof \ReflectionUnionType || $type instanceof \ReflectionIntersectionType) {
                $typeName = (string) $type;
                $isBuiltin = false;
            }

            $paramInfo[] = [
                'name' => $param->getName(),
                'type' => $typeName,
                'isBuiltin' => $isBuiltin,
                'allowsNull' => $param->allowsNull(),
                'isOptional' => $param->isOptional(),
                'hasDefaultValue' => $param->isDefaultValueAvailable(),
                'default' => $param->isDefaultValueAvailable() ? $param->getDefaultValue() : null,
                'position' => $param->getPosition(),
                'attributes' => array_map(
                    fn ($a) => $a->getName(),
                    $param->getAttributes()
                ),
            ];
        }

        return $paramInfo;
    }

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

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

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

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

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

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

    /**
     * @return array<string, mixed>
     */
    public function getStats(): Statistics
    {
        return Statistics::countersOnly([
            'parameters' => count($this->parameterCache),
            'parameter_info' => count($this->parameterInfoCache),
            'wrapped_parameters' => count($this->wrappedParameterCache),
        ]);
    }

    public function getParameterAttributes(ClassName $className, string $methodName, int $position): AttributeCollection
    {
        $key = "{$className->getFullyQualified()}::{$methodName}::param::{$position}::attributes";
        if (! isset($this->parameterCache[$key])) {
            $params = $this->getNativeParameters($className, $methodName);
            $this->parameterCache[$key] = isset($params[$position]) ? $params[$position]->getAttributes() : [];
        }

        return new AttributeCollection(...$this->parameterCache[$key]);
    }

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

        return isset($this->parameterCache[$cacheKey]) ||
               isset($this->parameterInfoCache[$cacheKey]) ||
               isset($this->wrappedParameterCache[$cacheKey]);
    }

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

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