<?php

declare(strict_types=1);

namespace App\Framework\DI;

use App\Framework\Core\ValueObjects\ClassName;
use App\Framework\DI\Dependency\ConstructorCache;
use App\Framework\DI\Dependency\ConstructorCacheEntry;
use App\Framework\DI\Dependency\Dependency;
use App\Framework\DI\Dependency\DependencyCollection;
use App\Framework\DI\Dependency\DependencyType;
use App\Framework\DI\Dependency\ResolvedParameter;
use App\Framework\DI\Exceptions\ParameterResolutionException;
use App\Framework\Reflection\ReflectionProvider;
use App\Framework\Reflection\WrappedReflectionParameter;
use ReflectionType;

/**
 * Löst Constructor-Abhängigkeiten auf
 */
final readonly class DependencyResolver
{
    private ConstructorCache $constructorCache;

    public function __construct(
        private ReflectionProvider $reflectionProvider,
        private Container $container
    ) {
        $this->constructorCache = new ConstructorCache();
    }

    public function resolveDependencies(ClassName $className): DependencyCollection
    {
        // Fast path: Check cache first
        if ($this->constructorCache->has($className)) {
            return $this->resolveFromCache($className);
        }

        $reflection = $this->reflectionProvider->getClass($className);

        // Check if class has constructor
        if (! $reflection->hasMethod('__construct')) {
            // Cache empty dependencies
            $this->constructorCache->set(ConstructorCacheEntry::empty($className));

            return DependencyCollection::empty();
        }

        $constructorParameters = $this->reflectionProvider->getMethodParameters($className, '__construct');

        if ($constructorParameters->isEmpty()) {
            // Cache empty dependencies
            $this->constructorCache->set(ConstructorCacheEntry::empty($className));

            return DependencyCollection::empty();
        }

        $dependencies = [];
        $cacheInfo = [];

        foreach ($constructorParameters as $param) {
            $resolvedParam = $this->resolveParameter($param, $className);
            $dependencies[] = $resolvedParam->value;
            $cacheInfo[] = $resolvedParam->getCacheInfo();
        }

        // Cache constructor info (even with non-cacheable dependencies for structure)
        $this->constructorCache->set(ConstructorCacheEntry::fromCacheInfo($className, $cacheInfo));

        return DependencyCollection::fromValues($dependencies);
    }

    private function resolveParameter(WrappedReflectionParameter $param, ClassName $className): ResolvedParameter
    {
        $type = $param->getType();

        // Strikte Validierung für Union/Intersection Types
        if ($type instanceof \ReflectionUnionType) {
            throw new ParameterResolutionException(
                paramName: $param->getName(),
                class: $className->getFullyQualified(),
                dependencyChain: [],
                reason: 'Union types are not supported in DI'
            );
        }

        if ($type instanceof \ReflectionIntersectionType) {
            throw new ParameterResolutionException(
                paramName: $param->getName(),
                class: $className->getFullyQualified(),
                dependencyChain: [],
                reason: 'Intersection types are not supported in DI'
            );
        }

        $dependency = $this->createDependency($param, $type, $className);

        return new ResolvedParameter($dependency->value, $dependency);
    }

    private function createDependency(WrappedReflectionParameter $param, ?ReflectionType $type, ClassName $className): Dependency
    {
        if ($param->hasDefaultValue()) {
            return Dependency::default($param->getDefaultValue());
        }

        $typeName = $param->getTypeName();

        if ($param->allowsNull() && $typeName !== null) {
            // Prüfe ob Container die nullable dependency bereitstellen kann
            if (! $param->isBuiltin() && $this->container->has($typeName)) {
                /** @var class-string<object> $typeName */
                $value = $this->container->get($typeName);

                return Dependency::nullableDependency($value, ClassName::create($typeName));
            }

            // Fallback auf null wenn nicht im Container verfügbar
            return Dependency::null();
        }

        if ($typeName === null || $param->isBuiltin()) {
            throw new ParameterResolutionException(
                paramName: $param->getName(),
                class: $className->getFullyQualified(),
                dependencyChain: [] // Wird vom Container gesetzt
            );
        }

        /** @var class-string<object> $typeName */
        $value = $this->container->get($typeName);

        return Dependency::dependency($value, ClassName::create($typeName));
    }

    private function resolveFromCache(ClassName $className): DependencyCollection
    {
        $cacheEntry = $this->constructorCache->get($className);

        if ($cacheEntry === null || ! $cacheEntry->hasParameters()) {
            return DependencyCollection::empty();
        }

        $dependencies = [];

        foreach ($cacheEntry->getParameters() as $paramInfo) {
            $dependencies[] = match ($paramInfo->type) {
                DependencyType::DEFAULT => $paramInfo->value,
                DependencyType::NULL => null,
                DependencyType::DEPENDENCY, DependencyType::NULLABLE_DEPENDENCY =>
                    $this->resolveFromContainer($paramInfo->className),
            };
        }

        return DependencyCollection::fromValues($dependencies);
    }

    private function resolveFromContainer(ClassName $className): object
    {
        return $this->container->get($className->getFullyQualified());
    }

    public function clearCache(ClassName $className): void
    {
        $this->constructorCache->remove($className);
    }

    public function flushCache(): void
    {
        $this->constructorCache->clear();
    }

    /**
     * Get cache statistics for debugging/monitoring
     */
    public function getCacheStats(): array
    {
        return $this->constructorCache->getStats();
    }

    /**
     * Get all classes that depend on the given class
     * @return array<ConstructorCacheEntry>
     */
    public function getDependentsOf(ClassName $className): array
    {
        return $this->constructorCache->getDependentsOf($className);
    }
}
