<?php

declare(strict_types=1);

namespace CloudXM\NFe\Bootstrap;

use Closure;
use CloudXM\SDK\Contracts\ContainerInterface;
use ReflectionClass;
use ReflectionException;
use ReflectionParameter;

/**
 * Dependency Injection Container
 *
 * Simple PSR-11 compliant container for dependency injection
 */
class Container implements ContainerInterface
{
    /**
     * Registered services
     *
     * @var array<string, mixed>
     */
    private array $services = [];

    /**
     * Singleton instances
     *
     * @var array<string, mixed>
     */
    private array $singletons = [];
    private bool $booted = false;

    /**
     * Register a singleton service
     *
     * @param string $id Service identifier
     * @param Closure|object|null $concrete Service concrete implementation
     * @return void
     */
    public function singleton(string $id, callable|string|null $concrete = null): void
    {
        if ($concrete === null) {
            // If no concrete provided, use class name
            $concrete = function () use ($id) {
                return $this->build($id);
            };
        }

        $this->services[$id] = [
            'type' => 'singleton',
            'concrete' => $concrete,
        ];
    }

    /**
     * Register a transient service
     *
     * @param string $id Service identifier
     * @param Closure|object|null $concrete Service concrete implementation
     * @return void
     */
    public function bind(string $id, callable|string|null $concrete = null, bool $shared = false): void
    {
        if ($concrete === null) {
            // If no concrete provided, use class name
            $concrete = function () use ($id) {
                return $this->build($id);
            };
        }

        $this->services[$id] = [
            'type' => $shared ? 'singleton' : 'transient',
            'concrete' => $concrete,
        ];
    }

    /**
     * Register an alias for a service
     */
    public function alias(string $abstract, string $alias): void
    {
        if (!isset($this->services[$abstract])) {
            // Lazy alias: resolve abstract when alias is requested.
            $this->services[$alias] = [
                'type' => 'singleton',
                'concrete' => function () use ($abstract) {
                    return $this->get($abstract);
                },
            ];
            return;
        }

        $this->services[$alias] = $this->services[$abstract];
    }

    /**
     * Get a service from container
     *
     * @param string $id Service identifier
     * @return mixed
     * @throws \Exception If service not found
     */
    public function get(string $id): mixed
    {
        if (!isset($this->services[$id])) {
            throw new \Exception("Service '{$id}' not found in container.");
        }

        $service = $this->services[$id];

        // Return singleton instance if already created
        if ($service['type'] === 'singleton' && isset($this->singletons[$id])) {
            return $this->singletons[$id];
        }

        // Build service
        $instance = $this->buildService($service);

        // Store singleton instance
        if ($service['type'] === 'singleton') {
            $this->singletons[$id] = $instance;
        }

        return $instance;
    }

    /**
     * Check if a service is registered
     *
     * @param string $id Service identifier
     * @return bool
     */
    public function has(string $id): bool
    {
        return isset($this->services[$id]);
    }

    /**
     * Boot the container
     */
    public function boot(): void
    {
        $this->booted = true;
    }

    /**
     * Check if container is booted
     */
    public function isBooted(): bool
    {
        return $this->booted;
    }

    /**
     * Get all registered service IDs
     */
    public function getServiceIds(): array
    {
        return array_keys($this->services);
    }

    /**
     * Build a service instance
     *
     * @param string $id Service identifier
     * @return mixed
     */
    private function build(string $id)
    {
        if (!class_exists($id)) {
            throw new \Exception("Class '{$id}' not found.");
        }

        return $this->buildService(['type' => 'transient', 'concrete' => $id]);
    }

    /**
     * Build a service from its definition
     *
     * @param array $service Service definition
     * @return mixed
     */
    private function buildService(array $service): mixed
    {
        $concrete = $service['concrete'];

        // If concrete is a closure, execute it
        if ($concrete instanceof Closure) {
            return $concrete($this);
        }

        // If concrete is a class name, build it
        if (is_string($concrete)) {
            return $this->buildClass($concrete);
        }

        // If concrete is already an object, return it
        return $concrete;
    }

    /**
     * Build a class with dependency injection
     *
     * @param string $className Class name
     * @return object
     */
    private function buildClass(string $className): object
    {
        try {
            $reflection = new ReflectionClass($className);
        } catch (ReflectionException $e) {
            throw new \Exception("Failed to reflect class '{$className}': {$e->getMessage()}", 0, $e);
        }

        $constructor = $reflection->getConstructor();

        // If no constructor, just instantiate
        if ($constructor === null) {
            $instance = $reflection->newInstance();
            return $instance;
        }

        // Get constructor parameters
        $parameters = $constructor->getParameters();
        $dependencies = [];

        foreach ($parameters as $parameter) {
            $dependencies[] = $this->resolveParameter($parameter);
        }

        // Instantiate with dependencies
        $instance = $reflection->newInstanceArgs($dependencies);
        return $instance;
    }

    /**
     * Resolve a constructor parameter
     *
     * @param ReflectionParameter $parameter Parameter to resolve
     * @return mixed
     */
    private function resolveParameter(ReflectionParameter $parameter)
    {
        // If parameter has a type, try to resolve from container
        $type = $parameter->getType();

        if ($type === null) {
            // No type hint, use default value
            if ($parameter->isDefaultValueAvailable()) {
                return $parameter->getDefaultValue();
            }

            throw new \Exception("Cannot resolve parameter '{$parameter->getName()}': no type hint and no default value.");
        }

        // Get class name from type
        $typeName = $type->getName();

        // If it's a built-in type, use default value
        if (in_array($typeName, ['int', 'string', 'bool', 'float', 'array', 'object'])) {
            if ($parameter->isDefaultValueAvailable()) {
                return $parameter->getDefaultValue();
            }

            throw new \Exception("Cannot resolve parameter '{$parameter->getName()}': built-in type '{$typeName}' has no default value.");
        }

        // Try to get from container
        if ($this->has($typeName)) {
            return $this->get($typeName);
        }

        // If not in container, try to build it
        if (class_exists($typeName)) {
            return $this->build($typeName);
        }

        // Cannot resolve
        throw new \Exception("Cannot resolve parameter '{$parameter->getName()}': type '{$typeName}' not found in container.");
    }

    /**
     * Reset container (useful for testing)
     *
     * @return void
     */
    public function reset(): void
    {
        $this->services = [];
        $this->singletons = [];
        $this->booted = false;
    }
}
