<?php
declare(strict_types=1);

namespace PpPhp\Core\Platform\DI;

use PpPhp\Core\Platform\DI\Contracts\ContainerInterface;
use PpPhp\Core\Platform\DI\Contracts\DefinitionInterface;
use PpPhp\Core\Platform\DI\Definition\AliasDefinition;
use PpPhp\Core\Platform\DI\Definition\ClassDefinition;
use PpPhp\Core\Platform\DI\Definition\FactoryDefinition;
use PpPhp\Core\Platform\DI\Definition\InstanceDefinition;
use PpPhp\Core\Platform\DI\Definition\ValueDefinition;
use PpPhp\Core\Platform\DI\Exceptions\MissingServiceException;
use PpPhp\Core\Platform\DI\Metadata\Lifetime;
use PpPhp\Core\Platform\DI\Repository\AliasRepository;
use PpPhp\Core\Platform\DI\Repository\DefinitionRepository;
use PpPhp\Core\Platform\DI\Repository\TagRepository;
use PpPhp\Core\Platform\DI\Resolver\DefinitionResolver;
use PpPhp\Core\Platform\DI\Scope\ScopeManager;

final class Container implements ContainerInterface
{
    private DefinitionRepository $definitions;
    private AliasRepository $aliases;
    private TagRepository $tags;
    private ScopeManager $scopes;
    private DefinitionResolver $resolver;
    private array $singletons = [];

    public function __construct(?DefinitionRepository $definitions = null, ?AliasRepository $aliases = null, ?TagRepository $tags = null, ?ScopeManager $scopes = null, ?DefinitionResolver $resolver = null)
    {
        $this->definitions = $definitions ?? new DefinitionRepository();
        $this->aliases = $aliases ?? new AliasRepository();
        $this->tags = $tags ?? new TagRepository();
        $this->scopes = $scopes ?? new ScopeManager();
        $this->resolver = $resolver ?? new DefinitionResolver();
        $this->instance(ContainerInterface::class, $this);
        $this->instance(self::class, $this);
    }

    public function has(string $id): bool
    {
        $id = $this->aliases->resolve($id);
        return $this->definitions->has($id) || class_exists($id);
    }

    public function get(string $id): mixed { return $this->make($id); }

    public function make(string $id): mixed
    {
        $id = $this->aliases->resolve($id);

        if (!$this->definitions->has($id) && class_exists($id)) {
            $this->register($id, $id);
        }

        $definition = $this->definitions->get($id);
        if (!$definition) {
            throw new MissingServiceException('Service not found: ' . $id);
        }

        if ($definition->lifetime() === Lifetime::SINGLETON && array_key_exists($id, $this->singletons)) {
            return $this->singletons[$id];
        }

        if ($definition->lifetime() === Lifetime::SCOPED && $this->scopes->current()->has($id)) {
            return $this->scopes->current()->get($id);
        }

        $resolved = $this->resolver->resolve($definition, $this);

        if (in_array($definition->lifetime(), [Lifetime::SINGLETON, Lifetime::INSTANCE], true)) {
            $this->singletons[$id] = $resolved;
        }

        if ($definition->lifetime() === Lifetime::SCOPED) {
            $this->scopes->current()->set($id, $resolved);
        }

        return $resolved;
    }

    public function register(string $id, ?string $className = null): RegistrationBuilder { return $this->setDefinition(new ClassDefinition($id, $className ?? $id)); }
    public function singleton(string $id, ?string $className = null): RegistrationBuilder { return $this->setDefinition(new ClassDefinition($id, $className ?? $id, Lifetime::SINGLETON)); }
    public function scoped(string $id, ?string $className = null): RegistrationBuilder { return $this->setDefinition(new ClassDefinition($id, $className ?? $id, Lifetime::SCOPED)); }
    public function prototype(string $id, ?string $className = null): RegistrationBuilder { return $this->setDefinition(new ClassDefinition($id, $className ?? $id, Lifetime::PROTOTYPE)); }
    public function factory(string $id, callable $factory): RegistrationBuilder { return $this->setDefinition(new FactoryDefinition($id, $factory)); }
    public function instance(string $id, object $instance): RegistrationBuilder { return $this->setDefinition(new InstanceDefinition($id, $instance)); }
    public function value(string $id, mixed $value): RegistrationBuilder { return $this->setDefinition(new ValueDefinition($id, $value)); }

    public function alias(string $alias, string $target): void
    {
        $this->aliases->add($alias, $target);
        $this->definitions->add(new AliasDefinition($alias, $target));
    }

    public function setDefinition(DefinitionInterface $definition): RegistrationBuilder
    {
        $this->definitions->add($definition);
        foreach ($definition->aliases() as $alias) { $this->aliases->add($alias, $definition->id()); }
        foreach ($definition->tags() as $tag => $attrs) { $this->tags->add($tag, $definition->id(), $attrs); }
        return new RegistrationBuilder($definition, $this->aliases, $this->tags);
    }

    public function tagged(string $tag): array
    {
        $items = [];
        foreach (array_keys($this->tags->get($tag)) as $id) {
            $items[] = $this->make($id);
        }
        return $items;
    }

    public function definitions(): array { return $this->definitions->all(); }
    public function definitionRepository(): DefinitionRepository { return $this->definitions; }
    public function aliasRepository(): AliasRepository { return $this->aliases; }
    public function tagRepository(): TagRepository { return $this->tags; }
    public function scopes(): ScopeManager { return $this->scopes; }

    public function toArray(): array
    {
        return [
            'definitions' => $this->definitions->toArray(),
            'aliases' => $this->aliases->all(),
            'tags' => $this->tags->all(),
            'singletons' => array_keys($this->singletons),
        ];
    }
}
