<?php

declare(strict_types=1);

namespace App\Framework\DI;

use App\Framework\Core\ValueObjects\ClassName;
use App\Framework\DI\Exceptions\CyclicDependencyException;
use App\Framework\DI\Exceptions\LazyLoadingException;
use App\Framework\Metrics\FrameworkMetricsCollector;
use App\Framework\Reflection\CachedReflectionProvider;
use App\Framework\Reflection\ReflectionProvider;
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(string $abstract, callable|string|object $concrete): void
    {
        $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(string $abstract, callable|string|object $concrete): void
    {
        $this->bind($abstract, $concrete);
        $this->instances->markAsSingleton($abstract);
    }

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

    /**
     * @inheritDoc
     */
    public function get(string|ClassName $class): object
    {
        $className = $class instanceof ClassName ? $class->toString() : $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 \RuntimeException(
                "Cannot resolve '{$class}': not a valid class and no binding exists. " .
                "Available bindings: " . implode(', ', 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 framework's method
            if (! $reflection->isInstantiable()) {
                $this->throwDetailedBindingException($class, $reflection);
            }

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

            return $reflection->newInstance(...$dependencies->toArray());
        } catch (\RuntimeException $e) {
            // If it's already our detailed exception, just re-throw
            if (str_contains($e->getMessage(), 'Dependency resolution chain:')) {
                throw $e;
            }

            // Otherwise, wrap with binding information
            throw new \RuntimeException(
                "Cannot resolve class '{$class}': {$e->getMessage()}. " .
                "Available bindings: " . implode(', ', array_keys($this->bindings->getAllBindings())) .
                ". Dependency chain: " . implode(' -> ', $this->resolving),
                0,
                $e
            );
        } catch (\ReflectionException $e) {
            throw new \RuntimeException(
                "Cannot resolve class '{$class}': {$e->getMessage()}. " .
                "Available bindings: " . implode(', ', array_keys($this->bindings->getAllBindings())) .
                ". Dependency chain: " . implode(' -> ', $this->resolving),
                0,
                $e
            );
        }
    }

    private function throwDetailedBindingException(string $class, $reflection): never
    {
        $availableBindings = array_keys($this->bindings->getAllBindings());
        $dependencyChain = implode(' -> ', $this->resolving);

        // Look for similar interface bindings
        $similarBindings = array_filter($availableBindings, function ($binding) use ($class) {
            return str_contains($binding, basename(str_replace('\\', '/', $class)));
        });

        $message = "Cannot instantiate class '{$class}': class is not instantiable (interface, abstract class, or trait).\n" .
                   "Dependency resolution chain: {$dependencyChain}\n" .
                   "Total available bindings: " . count($availableBindings) . "\n";

        if (! empty($similarBindings)) {
            $message .= "Similar bindings found: " . implode(', ', $similarBindings) . "\n";
        }

        $message .= "All bindings: " . implode(', ', $availableBindings);

        throw new \RuntimeException($message);
    }

    private function resolveBinding(string $class, callable|string|object $concrete): object
    {
        return match (true) {
            is_callable($concrete) => $concrete($this),
            is_string($concrete) => $this->get($concrete),
            default => $concrete
        };
    }

    /** @param class-string $class */
    public function has(string $class): bool
    {
        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(string $class): void
    {
        $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);
    }
}
