<?php

/**
 * The IoC (Inversion of Control) service container for the framework.
 * Centralizes registration, resolution, and lifecycle management of services
 * with automatic dependency injection, singleton binding, and aliasing.
 * Acts as the backbone for dependency management across the application.
 *
 * @package Framework
 * @since   1.0.0
 */
namespace Kirki\Framework;

\defined('ABSPATH') || exit;
use Closure;
use Exception;
use ReflectionClass;
use ReflectionParameter;
use TInstance;
use Kirki\Framework\Contracts\Container as ContainerContract;
use LogicException;
use ReflectionNamedType;
class Container implements ContainerContract
{
    /**
     * The instance of the container.
     *
     * @var static|null
     *
     * @since 1.0.0
     */
    protected static $instance = null;
    /**
     * The bindings of the container.
     *
     * @var array
     *
     * @since 1.0.0
     */
    protected array $bindings = [];
    /**
     * The instances of the container (singletons).
     *
     * @var array
     *
     * @since 1.0.0
     */
    protected array $instances = [];
    /**
     * The aliases for services.
     *
     * @var array
     *
     * @since 1.0.0
     */
    protected array $aliases = [];
    /**
     * Services currently being resolved (for circular dependency detection).
     *
     * @var array
     *
     * @since 1.0.0
     */
    protected array $resolved = [];
    /**
     * Tagged services.
     *
     * @var array
     *
     * @since 1.0.0
     */
    protected array $tags = [];
    /**
     * The base path of the application.
     *
     * @var string
     *
     * @since 1.0.0
     */
    protected $base_path;
    /**
     * Create a new container instance.
     *
     * @param string|null $base_path The base path of the application.
     *
     * @return void
     *
     * @since 1.0.0
     */
    public function __construct(?string $base_path = null)
    {
        $this->base_path = $base_path;
    }
    /**
     * Bind a class to the container.
     *
     * @param string $name The name of the service.
     * @param Closure|null $resolver The resolver for the service.
     *
     * @return void
     *
     * @since 1.0.0
     */
    public function bind(string $name, ?Closure $resolver = null)
    {
        if (\is_null($resolver)) {
            $resolver = function (Container $app, $params) use($name) {
                return $app->autowire($name, $params);
            };
        }
        $this->bindings[$name] = ['resolver' => $resolver, 'singleton' => \false];
    }
    /**
     * Bind a class to the container as a singleton (lazy loaded).
     *
     * @param string $name The name of the service.
     * @param Closure|null $resolver The resolver for the service.
     *
     * @return void
     *
     * @since 1.0.0
     */
    public function singleton(string $name, ?Closure $resolver = null)
    {
        if (\is_null($resolver)) {
            $resolver = function (Container $app, $params) use($name) {
                return $app->autowire($name, $params);
            };
        }
        $this->bindings[$name] = ['resolver' => $resolver, 'singleton' => \true];
    }
    /**
     * Bind an existing instance to the container.
     *
     * @template TInstance of mixed
     *
     * @param string $abstract The abstract name of the service.
     * @param TInstance $instance The instance of the service.
     *
     * @return TInstance
     *
     * @since 1.0.0
     */
    public function instance(string $abstract, $instance)
    {
        $bounded = $this->bound($abstract);
        unset($this->aliases[$abstract]);
        $this->instances[$abstract] = $instance;
        if ($bounded) {
            $this->rebound($abstract);
        }
        return $instance;
    }
    /**
     * Create an alias for a service.
     *
     * @param string $alias The alias name of the service.
     * @param string $abstract The abstract name of the service.
     *
     * @return void
     *
     * @throws \LogicException
     *
     * @since 1.0.0
     */
    public function alias(string $alias, string $abstract)
    {
        if ($alias === $abstract) {
            throw new LogicException(\sprintf('[%s] is aliased to itself.', $abstract));
        }
        $this->aliases[$alias] = $abstract;
    }
    /**
     * Tag services for grouped resolution.
     *
     * @param array $services The services to tag.
     * @param string $tag The tag to add the services to.
     *
     * @return void
     *
     * @since 1.0.0
     */
    public function tag(array $services, string $tag)
    {
        foreach ($services as $service) {
            $this->tags[$tag][] = $service;
        }
    }
    /**
     * Get all services with a given tag.
     *
     * @param string $tag The tag to get the services from.
     *
     * @return array
     *
     * @since 1.0.0
     */
    public function tagged(string $tag) : array
    {
        if (!isset($this->tags[$tag])) {
            return [];
        }
        return \array_map([$this, 'make'], $this->tags[$tag]);
    }
    /**
     * Check if a class has been resolved.
     *
     * @param string $name The name of the service.
     *
     * @return bool
     *
     * @since 1.0.0
     */
    protected function resolved(string $name) : bool
    {
        return \in_array($name, $this->resolved, \true);
    }
    /**
     * Make a class from the container.
     *
     * @param string $name The name of the service.
     * @param array $parameters The parameters to pass to the service.
     *
     * @return mixed
     *
     * @since 1.0.0
     */
    public function make(string $name, array $parameters = [])
    {
        return $this->resolve($name, $parameters);
    }
    /**
     * Resolve a class from the container.
     *
     * @param string $name The name of the service.
     * @param array $parameters The parameters to pass to the service.
     *
     * @return mixed
     *
     * @since 1.0.0
     */
    protected function resolve(string $name, array $parameters = [])
    {
        // Resolve aliases
        $name = $this->get_alias($name);
        // Return existing singleton instance
        if (isset($this->instances[$name])) {
            return $this->instances[$name];
        }
        // Use manual binding if available
        if (isset($this->bindings[$name])) {
            return $this->resolve_binding($name, $parameters);
        }
        // Attempt automatic resolution
        return $this->autowire($name, $parameters);
    }
    /**
     * Resolve a manually bound service.
     *
     * @param string $name The name of the service.
     * @param array $parameters The parameters to pass to the service.
     *
     * @return mixed
     *
     * @since 1.0.0
     */
    protected function resolve_binding(string $name, array $parameters = [])
    {
        $binding = $this->bindings[$name];
        $instance = $binding['resolver']($this, $parameters);
        // Store singleton instances
        if (!empty($binding['singleton'])) {
            $this->instances[$name] = $instance;
        }
        return $instance;
    }
    /**
     * Automatically wire dependencies using reflection.
     *
     * @param string $class The class to autowire.
     * @param array $parameters The parameters to pass to the class.
     *
     * @return mixed
     *
     * @throws \Exception
     *
     * @since 1.0.0
     */
    protected function autowire(string $class, array $parameters = [])
    {
        // Check for circular dependencies
        if ($this->resolved($class)) {
            $chain = \implode(' → ', $this->resolved) . " → {$class}";
            throw new Exception(\sprintf('Circular dependency detected: %s', $chain));
        }
        $this->resolved[] = $class;
        try {
            $reflector = new ReflectionClass($class);
            if (!$reflector->isInstantiable()) {
                throw new Exception(\sprintf('Class "%s" is not instantiable.', $class));
            }
            $constructor = $reflector->getConstructor();
            if (!$constructor) {
                return new $class();
            }
            $dependencies = $this->resolve_dependencies($constructor->getParameters(), $parameters);
            return $reflector->newInstanceArgs($dependencies);
        } finally {
            \array_pop($this->resolved);
        }
    }
    /**
     * Resolve constructor dependencies.
     *
     * @param array $parameters The parameters to pass to the dependencies.
     * @param array $primitives The primitives to pass to the dependencies.
     *
     * @return array
     *
     * @since 1.0.0
     */
    protected function resolve_dependencies(array $parameters, array $primitives = []) : array
    {
        $dependencies = [];
        foreach ($parameters as $parameter) {
            $dependency = $parameter->getType();
            if ($dependency === null || $dependency->isBuiltin()) {
                // Handle primitive parameters
                $dependencies[] = $this->resolve_primitive($parameter, $primitives);
            } else {
                // Handle class parameters
                $dependency_name = $dependency instanceof ReflectionNamedType ? $dependency->getName() : (string) $dependency;
                $dependencies[] = $this->make($dependency_name);
            }
        }
        return $dependencies;
    }
    /**
     * Resolve primitive parameter.
     *
     * @param ReflectionParameter $parameter The parameter to resolve.
     * @param array $primitives The primitives to pass to the dependencies.
     *
     * @return mixed
     *
     * @throws \Exception
     *
     * @since 1.0.0
     */
    protected function resolve_primitive(ReflectionParameter $parameter, array $primitives)
    {
        $param_name = $parameter->getName();
        if (\array_key_exists($param_name, $primitives)) {
            return $primitives[$param_name];
        }
        if ($parameter->isDefaultValueAvailable()) {
            return $parameter->getDefaultValue();
        }
        throw new Exception(\sprintf('Unable to resolve primitive parameter "%s" in class "%s".', $param_name, $parameter->getDeclaringClass()->getName()));
    }
    /**
     * Get the alias for a service.
     *
     * @param string $name The name of the service.
     *
     * @return string
     *
     * @since 1.0.0
     */
    protected function get_alias(string $name) : string
    {
        return $this->aliases[$name] ?? $name;
    }
    /**
     * Check if a class exists in the container.
     *
     * @param string $name The name of the service.
     *
     * @return bool
     *
     * @since 1.0.0
     */
    public function has(string $name) : bool
    {
        return $this->bound($name);
    }
    /**
     * Check if a name is an alias.
     *
     * @param string $name The name of the service.
     *
     * @return bool
     *
     * @since 1.0.0
     */
    public function is_alias(string $name) : bool
    {
        return isset($this->aliases[$name]);
    }
    /**
     * Check if a name is bound in the container.
     *
     * @param string $abstract The abstract name of the service.
     *
     * @return bool
     *
     * @since 1.0.0
     */
    public function bound($abstract)
    {
        return isset($this->bindings[$abstract]) || isset($this->instances[$abstract]) || $this->is_alias($abstract);
    }
    /**
     * Rebind a name in the container.
     *
     * @param string $abstract The abstract name of the service.
     *
     * @return void
     *
     * @since 1.0.0
     */
    public function rebound($abstract)
    {
        $this->make($abstract);
    }
    /**
     * Forget an instance from the container.
     *
     * @param string $abstract The abstract name of the service.
     *
     * @return void
     *
     * @since 1.0.0
     */
    public function forget_instance($abstract)
    {
        unset($this->instances[$abstract]);
    }
    /**
     * Forget all instances from the container.
     *
     * @return void
     *
     * @since 1.0.0
     */
    public function forget_instances()
    {
        $this->instances = [];
    }
    /**
     * Get the instance of the container.
     *
     * @param string|null $base_path The base path of the application.
     *
     * @return static
     *
     * @since 1.0.0
     */
    public static function get_instance(?string $base_path = null)
    {
        if (\is_null(static::$instance)) {
            static::$instance = new static($base_path);
        }
        return static::$instance;
    }
    /**
     * Flush all bindings and instances.
     *
     * @return void
     *
     * @since 1.0.0
     */
    public function flush()
    {
        $this->bindings = [];
        $this->instances = [];
        $this->aliases = [];
        $this->resolved = [];
        $this->tags = [];
    }
}
