<?php

declare(strict_types=1);

namespace App\Framework\DI;

use App\Framework\Core\ValueObjects\ClassName;
use App\Framework\DI\Exceptions\ClassNotInstantiable;
use App\Framework\DI\Exceptions\ClassNotResolvableException;
use App\Framework\DI\Exceptions\ClassResolutionException;
use App\Framework\DI\Exceptions\ContainerException;
use App\Framework\DI\Exceptions\CyclicDependencyException;
use App\Framework\DI\Exceptions\LazyLoadingException;
use App\Framework\Discovery\Results\DiscoveryRegistry;
use App\Framework\Metrics\FrameworkMetricsCollector;
use App\Framework\Reflection\CachedReflectionProvider;
use App\Framework\Reflection\ReflectionProvider;
use Stringable;
use Throwable;

final class DefaultContainer implements Container
{
    /** @var class-string[] */
    private array $resolving = [];

    private readonly DependencyResolver $dependencyResolver;

    private readonly SingletonDetector $singletonDetector;

    private readonly LazyInstantiator $lazyInstantiator;

    public readonly MethodInvoker $invoker;

    public readonly ContainerIntrospector $introspector;

    public readonly FrameworkMetricsCollector $metrics;

    public function __construct(
        private readonly InstanceRegistry $instances = new InstanceRegistry(),
        private readonly BindingRegistry $bindings = new BindingRegistry(),
        private readonly ReflectionProvider $reflectionProvider = new CachedReflectionProvider(),
    ) {
        $this->dependencyResolver = new DependencyResolver($this->reflectionProvider, $this);
        $this->singletonDetector = new SingletonDetector($this->reflectionProvider, $this->instances);
        $this->lazyInstantiator = new LazyInstantiator(
            $this->reflectionProvider,
            $this->createInstance(...)
        );
        $this->invoker = new MethodInvoker($this, $this->reflectionProvider);
        $this->introspector = new ContainerIntrospector(
            $this,
            $this->instances,
            $this->bindings,
            $this->reflectionProvider,
            fn (): array => $this->resolving
        );
        $this->metrics = new FrameworkMetricsCollector();

        $this->registerSelf();
        $this->instance(ReflectionProvider::class, $this->reflectionProvider);
    }

    public function bind(ClassName|Stringable|string $abstract, callable|string|object $concrete): void
    {
        $abstract = (string) $abstract;
        $this->bindings->bind($abstract, $concrete);

        // Only clear caches for valid class names, skip string keys like 'filesystem.storage.local'
        if (class_exists($abstract) || interface_exists($abstract)) {
            $this->clearCaches(ClassName::create($abstract));
        }
    }

    public function singleton(ClassName|Stringable|string $abstract, callable|string|object $concrete): void
    {
        $abstract = (string) $abstract;
        $this->bind($abstract, $concrete);
        $this->instances->markAsSingleton($abstract);
    }

    public function instance(ClassName|Stringable|string $abstract, object $instance): void
    {
        $this->instances->setInstance($abstract, $instance);
    }

    /**
     * @template T of object
     * @param class-string<T>|ClassName|Stringable $class
     * @return T
     */
    public function get(ClassName|Stringable|string $class): object
    {
        $className = (string) $class;

        // Bereits instanziierte Objekte zurückgeben
        if ($this->instances->hasSingleton($className)) {
            $singleton = $this->instances->getSingleton($className);
            if ($singleton !== null) {
                return $singleton;
            }
        }

        if ($this->instances->hasInstance($className)) {
            return $this->instances->getInstance($className);
        }

        // Lazy Loading versuchen
        if ($this->lazyInstantiator->canUseLazyLoading($className, $this->instances)) {
            try {
                return $this->lazyInstantiator->createLazyInstance($className);
            } catch (Throwable $e) {
                if (! $e instanceof LazyLoadingException) {
                    throw $e;
                }
            }
        }

        return $this->createInstance($className);
    }

    /**
     * @template T of object
     * @param class-string<T> $class
     * @return T
     */
    private function createInstance(string $class): object
    {
        if (in_array($class, $this->resolving, true)) {
            throw new CyclicDependencyException(
                dependencyChain: $this->resolving,
                class: $class
            );
        }

        $this->resolving[] = $class;

        try {
            $instance = $this->buildInstance($class);

            // Only check singleton attribute for actual classes
            if ((class_exists($class) || interface_exists($class)) &&
                $this->singletonDetector->isSingleton(ClassName::create($class))) {
                $this->instances->setSingleton($class, $instance);
            } else {
                $this->instances->setInstance($class, $instance);
            }

            return $instance;
        } catch (Throwable $e) {
            // Track resolution failures
            $this->metrics->increment('container.resolve.failures');

            throw $e;
        } finally {
            array_pop($this->resolving);
        }
    }

    private function buildInstance(string $class): object
    {
        if ($this->bindings->hasBinding($class)) {
            return $this->resolveBinding($class, $this->bindings->getBinding($class));
        }

        // For string keys without bindings, throw immediately
        if (!class_exists($class) && !interface_exists($class)) {
            throw new ClassNotResolvableException(
                class: $class,
                availableBindings: array_keys($this->bindings->getAllBindings())
            );
        }

        $className = ClassName::create($class);

        // Enhanced diagnostics for missing bindings
        try {
            $reflection = $this->reflectionProvider->getClass($className);

            // Check if class is instantiable using the framework's method
            if (! $reflection->isInstantiable()) {
                $this->throwDetailedBindingException($class/*, $reflection*/);
            }

            $dependencies = $this->dependencyResolver->resolveDependencies($className);

            return $reflection->newInstance(...$dependencies->toArray());
        } catch (ContainerException $e) {
            // If it's already a ContainerException, just re-throw
            throw $e;
        } catch (\RuntimeException|\ReflectionException $e) {
            // Wrap with binding information
            throw ClassResolutionException::fromRuntimeException(
                class: $class,
                exception: $e,
                availableBindings: array_keys($this->bindings->getAllBindings()),
                dependencyChain: $this->resolving
            );
        }
    }

    private function throwDetailedBindingException(string $class/*, $reflection*/): never
    {
        $availableBindings = array_keys($this->bindings->getAllBindings());

        // Try to get DiscoveryRegistry from container and include discovered initializers
        $discoveredInitializers = [];
        if ($this->has(DiscoveryRegistry::class)) {
            try {
                $discoveryRegistry = $this->get(DiscoveryRegistry::class);
                $initializerResults = $discoveryRegistry->attributes->get(Initializer::class);

                if (! empty($initializerResults)) {
                    $discoveredInitializers = array_map(
                        fn($attr) => $attr->className->getFullyQualified(),
                        $initializerResults
                    );
                }
            } catch (\Throwable $e) {
                // Silently ignore errors when trying to access DiscoveryRegistry to avoid masking the original error
            }
        }

        throw ClassNotInstantiable::fromContainerContext(
            class: $class,
            dependencyChain: $this->resolving,
            availableBindings: $availableBindings,
            discoveredInitializers: $discoveredInitializers
        );
    }

    private function resolveBinding(string $class, callable|string|object $concrete): object
    {
        try {
            return match (true) {
                is_callable($concrete) => $concrete($this),
                is_string($concrete) => $this->get($concrete),
                /* @var object $concrete */
                default => $concrete
            };
        } catch (ContainerException $e) {
            // Re-throw ContainerExceptions as-is (they already have proper context)
            throw $e;
        } catch (\Throwable $e) {
            // Determine binding type for better error messages
            $bindingType = match (true) {
                is_callable($concrete) => 'callable',
                is_string($concrete) => 'string',
                default => 'object'
            };

            throw ClassResolutionException::fromBindingResolution(
                class: $class,
                previous: $e,
                availableBindings: array_keys($this->bindings->getAllBindings()),
                dependencyChain: $this->resolving,
                bindingType: $bindingType
            );
        }
    }

    /** @param class-string $class */
    public function has(ClassName|Stringable|string $class): bool
    {
        $class = (string) $class;
        return $this->instances->hasSingleton($class)
            || $this->instances->hasInstance($class)
            || $this->bindings->hasBinding($class)
            || (! empty($class) && ClassName::create($class)->exists() && $this->reflectionProvider->getClass(ClassName::create($class))->isInstantiable());
    }

    /** @param class-string $class */
    public function forget(ClassName|Stringable|string $class): void
    {
        $class = (string) $class;
        $this->instances->forget($class);
        $this->bindings->forget($class);
        $this->clearCaches(ClassName::create($class));
    }

    public function flush(): void
    {
        $this->instances->flush();
        $this->bindings->flush();
        $this->reflectionProvider->flush();
        $this->dependencyResolver->flushCache();
        $this->lazyInstantiator->flushFactories();

        // Container selbst wieder registrieren
        $this->registerSelf();
    }

    public function getRegisteredServices(): array
    {
        return array_merge(
            $this->instances->getAllRegistered(),
            $this->bindings->getAllBindings()
        );
    }

    /**
     * Get all registered service IDs (for debugging/admin)
     * @return array<string>
     */
    public function getServiceIds(): array
    {
        return array_keys($this->getRegisteredServices());
    }

    private function clearCaches(ClassName $className): void
    {
        $this->reflectionProvider->forget($className);
        $this->dependencyResolver->clearCache($className);
        $this->lazyInstantiator->forgetFactory($className->getFullyQualified());
    }

    private function registerSelf(): void
    {
        $this->instances->setSingleton(self::class, $this);
        $this->instances->setSingleton(DefaultContainer::class, $this);
        $this->instances->setSingleton(Container::class, $this);
    }
}
