<?php

declare(strict_types=1);

namespace BotGuard\Core;

use Closure;
use RuntimeException;

/**
 * A small dependency-injection container.
 *
 * Supports binding factories, singletons and pre-built instances, plus
 * autowiring of constructor dependencies via reflection so controllers can
 * type-hint framework services (Database, Request, ...) without manual glue.
 */
final class Container
{
    /** @var array<string, Closure> */
    private array $bindings = [];

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

    /** @var array<string, bool> */
    private array $shared = [];

    private static ?Container $global = null;

    /**
     * Access the process-wide container instance.
     */
    public static function getInstance(): Container
    {
        return self::$global ??= new self();
    }

    public static function setInstance(Container $container): void
    {
        self::$global = $container;
    }

    /**
     * Register a transient factory binding.
     */
    public function bind(string $id, Closure $factory): void
    {
        $this->bindings[$id] = $factory;
        $this->shared[$id] = false;
    }

    /**
     * Register a shared (singleton) factory binding.
     */
    public function singleton(string $id, Closure $factory): void
    {
        $this->bindings[$id] = $factory;
        $this->shared[$id] = true;
    }

    /**
     * Store an already-constructed instance as a singleton.
     */
    public function instance(string $id, object $instance): void
    {
        $this->instances[$id] = $instance;
        $this->shared[$id] = true;
    }

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

    /**
     * Resolve an entry, autowiring concrete classes when no binding exists.
     */
    public function make(string $id): mixed
    {
        if (isset($this->instances[$id])) {
            return $this->instances[$id];
        }

        if (isset($this->bindings[$id])) {
            $object = ($this->bindings[$id])($this);

            if (($this->shared[$id] ?? false) && is_object($object)) {
                $this->instances[$id] = $object;
            }

            return $object;
        }

        // Fall back to reflection-based autowiring for concrete classes.
        return $this->build($id);
    }

    /**
     * Instantiate a concrete class, recursively resolving its dependencies.
     */
    private function build(string $class): object
    {
        if (! class_exists($class)) {
            throw new RuntimeException("Container cannot resolve unknown binding [{$class}].");
        }

        $reflector = new \ReflectionClass($class);

        if (! $reflector->isInstantiable()) {
            throw new RuntimeException("Class [{$class}] is not instantiable.");
        }

        $constructor = $reflector->getConstructor();

        if ($constructor === null) {
            return new $class();
        }

        $arguments = [];

        foreach ($constructor->getParameters() as $parameter) {
            $type = $parameter->getType();

            if ($type instanceof \ReflectionNamedType && ! $type->isBuiltin()) {
                $arguments[] = $this->make($type->getName());

                continue;
            }

            if ($parameter->isDefaultValueAvailable()) {
                $arguments[] = $parameter->getDefaultValue();

                continue;
            }

            throw new RuntimeException(
                "Cannot autowire parameter \${$parameter->getName()} of [{$class}]."
            );
        }

        return $reflector->newInstanceArgs($arguments);
    }
}
