<?php

namespace App\Framework\DI;

use App\Framework\DI\Exceptions\CyclicDependencyException;
use App\Framework\DI\Exceptions\LazyLoadingException;
use App\Framework\Reflection\CachedReflectionProvider;
use App\Framework\Reflection\ReflectionProvider;

use Throwable;

final class DefaultContainer implements Container
{
    private array $resolving = [];
    private readonly DependencyResolver $dependencyResolver;
    private readonly SingletonDetector $singletonDetector;
    private readonly LazyInstantiator $lazyInstantiator;
    public readonly MethodInvoker $invoker;

    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->registerSelf();
    }

    public function bind(string $abstract, callable|string|object $concrete): void
    {
        $this->bindings->bind($abstract, $concrete);
        $this->clearCaches($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);
    }

    /**
     * @template T of object
     * @param class-string<T> $class
     * @return T
     */
    public function get(string $class): object
    {
        // Bereits instanziierte Objekte zurückgeben
        if ($this->instances->hasSingleton($class)) {
            return $this->instances->getSingleton($class);
        }

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

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

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

    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);

            if ($this->singletonDetector->isSingleton($class)) {
                $this->instances->setSingleton($class, $instance);
            } else {
                $this->instances->setInstance($class, $instance);
            }

            return $instance;
        } finally {
            array_pop($this->resolving);
        }
    }

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

        $reflection = $this->reflectionProvider->getClass($class);
        $dependencies = $this->dependencyResolver->resolveDependencies($class);

        return $reflection->newInstanceArgs($dependencies);
    }

    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
        };
    }

    public function has(string $class): bool
    {
        return $this->instances->hasSingleton($class)
            || $this->instances->hasInstance($class)
            || $this->bindings->hasBinding($class)
            || class_exists($class);
    }

    public function forget(string $class): void
    {
        $this->instances->forget($class);
        $this->bindings->forget($class);
        $this->clearCaches($class);
    }

    public function flush(): void
    {
        $this->instances->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()
        );
    }

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

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