<?php
namespace Api\Core;

/**
 * Simple service container / dependency injector.
 *
 * - bind($abstract, $concrete): registra una factory, callable o nome classe.
 * - singleton($abstract, $concrete): come bind ma mantiene l'istanza.
 * - make($abstract): risolve e restituisce l'istanza.
 *
 * Nota: usa reflection per risolvere le dipendenze del costruttore.
 */
class Container {
    /** @var array<string,mixed> */
    private $bindings = [];

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

    /**
     * Bind an abstract name (interface or key) to a concrete implementation.
     * $concrete can be:
     *  - a callable: function(Container $c) { return new Foo(); }
     *  - a string class name: 'App\\Foo'
     *
     * @param string $abstract
     * @param callable|string $concrete
     * @return void
     */
    public function bind(string $abstract, $concrete): void {
        $this->bindings[$abstract] = $concrete;
    }

    /**
     * Bind as singleton. The first resolved instance is cached and returned afterwards.
     *
     * @param string $abstract
     * @param callable|string $concrete
     * @return void
     */
    public function singleton(string $abstract, $concrete): void {
        $this->bindings[$abstract] = $concrete;
        // mark as singleton with null placeholder
        $this->instances[$abstract] = null;
    }

    /**
     * Resolve an abstract to an instance.
     *
     * @param string $abstract
     * @return mixed
     * @throws \Exception
     */
    public function make(string $abstract) {
        // return existing singleton instance if already created
        if (array_key_exists($abstract, $this->instances) && $this->instances[$abstract] !== null) {
            return $this->instances[$abstract];
        }

        if (!isset($this->bindings[$abstract])) {
            // if no binding, try to instantiate the abstract directly if it's a class
            if (class_exists($abstract)) {
                $object = $this->build($abstract);
            } else {
                throw new \Exception("No binding found for [{$abstract}]");
            }
        } else {
            $concrete = $this->bindings[$abstract];

            if (is_callable($concrete)) {
                // factory receives the container
                $object = $concrete($this);
            } elseif (is_string($concrete) && class_exists($concrete)) {
                $object = $this->build($concrete);
            } else {
                throw new \Exception("Invalid binding for [{$abstract}]");
            }
        }

        // if abstract was registered as singleton, cache the instance
        if (array_key_exists($abstract, $this->instances)) {
            $this->instances[$abstract] = $object;
        }

        return $object;
    }

    /**
     * Build an instance of the given class resolving constructor dependencies.
     *
     * @param string $class
     * @return object
     * @throws \Exception
     */
    private function build(string $class) {
        $reflector = new \ReflectionClass($class);

        if (!$reflector->isInstantiable()) {
            throw new \Exception("Class {$class} is not instantiable");
        }

        $constructor = $reflector->getConstructor();
        if (is_null($constructor)) {
            return new $class();
        }

        $params = $constructor->getParameters();
        $dependencies = [];

        foreach ($params as $param) {
            $type = $param->getType();

            // If parameter has a class/interface type, resolve it from container
            if ($type && !$type->isBuiltin()) {
                $depClass = $type->getName();
                $dependencies[] = $this->make($depClass);
                continue;
            }

            // If default value is available, use it
            if ($param->isDefaultValueAvailable()) {
                $dependencies[] = $param->getDefaultValue();
                continue;
            }

            // Cannot resolve the dependency
            $name = $param->getName();
            throw new \Exception("Unresolvable dependency [\${$name}] in class {$class}");
        }

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