<?php
declare(strict_types=1);

namespace Celena\Core;

use Closure;
use ReflectionClass;
use ReflectionNamedType;
use ReflectionParameter;
use RuntimeException;

/**
 * Минималистичный DI-контейнер.
 * - bind() — фабрика, новый экземпляр на каждый make().
 * - singleton() — один экземпляр на всё приложение.
 * - instance() — заранее построенный объект.
 * - make() — авто-резолвинг через рефлексию.
 */
final class Container
{
    /** @var array<string, array{factory:Closure, shared:bool}> */
    private array $bindings = [];

    /** @var array<string, mixed> */
    private array $instances = [];

    public function bind(string $abstract, Closure|string $concrete): void
    {
        $this->bindings[$abstract] = [
            'factory' => $this->wrap($concrete),
            'shared' => false,
        ];
    }

    public function singleton(string $abstract, Closure|string $concrete): void
    {
        $this->bindings[$abstract] = [
            'factory' => $this->wrap($concrete),
            'shared' => true,
        ];
    }

    public function instance(string $abstract, mixed $instance): void
    {
        $this->instances[$abstract] = $instance;
    }

    public function has(string $abstract): bool
    {
        return isset($this->bindings[$abstract]) || isset($this->instances[$abstract]);
    }

    public function make(string $abstract, array $params = []): mixed
    {
        if (isset($this->instances[$abstract])) {
            return $this->instances[$abstract];
        }

        if (isset($this->bindings[$abstract])) {
            $binding = $this->bindings[$abstract];
            $object = ($binding['factory'])($this, $params);
            if ($binding['shared']) {
                $this->instances[$abstract] = $object;
            }
            return $object;
        }

        // авто-резолвинг через рефлексию
        return $this->build($abstract, $params);
    }

    private function wrap(Closure|string $concrete): Closure
    {
        if ($concrete instanceof Closure) {
            return $concrete;
        }
        return fn (Container $c, array $params) => $c->build($concrete, $params);
    }

    private function build(string $class, array $params): object
    {
        if (!class_exists($class)) {
            throw new RuntimeException("Cannot resolve [$class]: class not found.");
        }
        $ref = new ReflectionClass($class);
        if (!$ref->isInstantiable()) {
            throw new RuntimeException("Cannot instantiate [$class].");
        }
        $ctor = $ref->getConstructor();
        if ($ctor === null) {
            return new $class();
        }
        $args = [];
        foreach ($ctor->getParameters() as $param) {
            $args[] = $this->resolveParameter($param, $params);
        }
        return $ref->newInstanceArgs($args);
    }

    private function resolveParameter(ReflectionParameter $param, array $overrides): mixed
    {
        $name = $param->getName();
        if (array_key_exists($name, $overrides)) {
            return $overrides[$name];
        }
        $type = $param->getType();
        if ($type instanceof ReflectionNamedType && !$type->isBuiltin()) {
            return $this->make($type->getName());
        }
        if ($param->isDefaultValueAvailable()) {
            return $param->getDefaultValue();
        }
        if ($param->allowsNull()) {
            return null;
        }
        throw new RuntimeException("Cannot resolve parameter \${$name}.");
    }
}
