<?php
declare(strict_types=1);

namespace App\Foundation;

use App\Foundation\Contracts\ServiceContainerInterface;
use ReflectionClass;
use ReflectionNamedType;
use RuntimeException;

final class Container implements ServiceContainerInterface
{
    /** @var array<string,mixed> */
    private array $services = [];
    /** @var array<string,callable> */
    private array $factories = [];
    /** @var array<string,string> */
    private array $aliases = [];
    /** @var array<string,bool> */
    private array $resolving = [];

    public function set(string $id, mixed $service): void
    {
        $this->services[$id] = $service;
        unset($this->factories[$id]);
    }

    public function singleton(string $id, callable $factory): void
    {
        $this->factories[$id] = $factory;
        unset($this->services[$id]);
    }

    public function alias(string $alias, string $id): void
    {
        $this->aliases[$alias] = $id;
    }

    public function has(string $id): bool
    {
        $id = $this->resolveAlias($id);
        return array_key_exists($id, $this->services)
            || array_key_exists($id, $this->factories)
            || class_exists($id);
    }

    public function get(string $id): mixed
    {
        $id = $this->resolveAlias($id);
        if (array_key_exists($id, $this->services)) {
            return $this->services[$id];
        }
        if (array_key_exists($id, $this->factories)) {
            return $this->resolveFactory($id);
        }
        if (class_exists($id)) {
            return $this->autowire($id);
        }
        throw new RuntimeException('Service AXIOM introuvable : ' . $id);
    }

    /** @return list<string> */
    public function ids(): array
    {
        $ids = array_unique(array_merge(array_keys($this->services), array_keys($this->factories), array_keys($this->aliases)));
        sort($ids);
        return $ids;
    }

    private function resolveFactory(string $id): mixed
    {
        if (($this->resolving[$id] ?? false) === true) {
            throw new RuntimeException('Dépendance circulaire détectée : ' . $id);
        }
        $this->resolving[$id] = true;
        try {
            $service = ($this->factories[$id])($this);
            $this->services[$id] = $service;
            unset($this->factories[$id]);
            return $service;
        } finally {
            unset($this->resolving[$id]);
        }
    }

    private function autowire(string $id): object
    {
        if (($this->resolving[$id] ?? false) === true) {
            throw new RuntimeException('Dépendance circulaire détectée : ' . $id);
        }
        $this->resolving[$id] = true;
        try {
            $reflection = new ReflectionClass($id);
            if (!$reflection->isInstantiable()) {
                throw new RuntimeException('Service non instanciable : ' . $id);
            }
            $constructor = $reflection->getConstructor();
            if ($constructor === null) {
                $object = $reflection->newInstance();
            } else {
                $arguments = [];
                foreach ($constructor->getParameters() as $parameter) {
                    $type = $parameter->getType();
                    if ($type instanceof ReflectionNamedType && !$type->isBuiltin()) {
                        $arguments[] = $this->get($type->getName());
                    } elseif ($parameter->isDefaultValueAvailable()) {
                        $arguments[] = $parameter->getDefaultValue();
                    } elseif ($parameter->allowsNull()) {
                        $arguments[] = null;
                    } else {
                        throw new RuntimeException(sprintf('Impossible de résoudre %s::$%s', $id, $parameter->getName()));
                    }
                }
                $object = $reflection->newInstanceArgs($arguments);
            }
            $this->services[$id] = $object;
            return $object;
        } finally {
            unset($this->resolving[$id]);
        }
    }

    private function resolveAlias(string $id): string
    {
        $seen = [];
        while (isset($this->aliases[$id])) {
            if (isset($seen[$id])) {
                throw new RuntimeException('Boucle d’alias détectée : ' . $id);
            }
            $seen[$id] = true;
            $id = $this->aliases[$id];
        }
        return $id;
    }
}
