<?php
/**
 * BookiFlex Core
 *
 * @package    BookiFlex
 * @subpackage Core
 * @author     BookiFlex
 * @copyright  Copyright (c) 2025 BookiFlex
 * @license    GPL-2.0-or-later
 *
 * @link       https://bookiflex.com
 */

declare(strict_types=1);

namespace BookiFlex\Core\Container;

use Pimple\Container;
use Pimple\ServiceProviderInterface;

defined( 'ABSPATH' ) || exit;

/**
 * Service container based on Pimple
 */
class ServiceContainer extends Container
{
    /**
     * Resolved instances cache for make() method
     */
    private array $resolved = [];

    /**
     * Register a service provider
     */
    public function register(ServiceProviderInterface $provider, array $values = []): self
    {
        parent::register($provider, $values);
        return $this;
    }

    /**
     * Make an instance of a class with automatic dependency injection
     *
     * @template T
     * @param class-string<T> $class
     * @return object
     */
    public function make(string $class): object
    {
        // Return from cache if already resolved
        if (isset($this->resolved[$class])) {
            return $this->resolved[$class];
        }

        // Check if registered in container
        if (isset($this[$class])) {
            $instance = $this[$class];
            $this->resolved[$class] = $instance;
            return $instance;
        }

        // Try to auto-resolve
        try {
            $instance = $this->_resolveClass($class);
            $this->resolved[$class] = $instance;
            return $instance;
        } catch (\Exception $e) {
            throw new \RuntimeException(
                sprintf('Unable to resolve class "%s": %s', $class, $e->getMessage()),
                0,
                $e
            );
        }
    }

    /**
     * Bind a singleton
     */
    public function singleton(string $abstract, $concrete = null): void
    {
        $this[$abstract] = function ($c) use ($abstract, $concrete) {
            if ($concrete === null) {
                $concrete = $abstract;
            }

            if (is_callable($concrete)) {
                return $concrete($c);
            }

            return $this->_resolveClass($concrete);
        };
    }

    /**
     * Bind a factory (new instance each time)
     */
    public function bind(string $abstract, $concrete = null): void
    {
        $this[$abstract] = $this->factory(function ($c) use ($abstract, $concrete) {
            if ($concrete === null) {
                $concrete = $abstract;
            }

            if (is_callable($concrete)) {
                return $concrete($c);
            }

            return $this->_resolveClass($concrete);
        });
    }

    /**
     * Check if service exists
     */
    public function has(string $id): bool
    {
        return isset($this[$id]);
    }

    /**
     * Get a service (alias for ArrayAccess)
     */
    public function get(string $id)
    {
        return $this[$id];
    }

    /**
     * Resolve a class with dependency injection
     */
    private function _resolveClass(string $class): object
    {
        $reflector = new \ReflectionClass($class);

        if (!$reflector->isInstantiable()) {
            throw new \RuntimeException(
                sprintf(
                    'Class %s is not instantiable',
                    $class
                )
            );
        }

        $constructor = $reflector->getConstructor();

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

        $dependencies = $this->_resolveDependencies($constructor);

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

    /**
     * Resolve constructor dependencies
     */
    private function _resolveDependencies(\ReflectionMethod $method): array
    {
        $dependencies = [];

        foreach ($method->getParameters() as $parameter) {
            $dependency = $this->_resolveDependency($parameter);

            if ($dependency !== null) {
                $dependencies[] = $dependency;
            }
        }

        return $dependencies;
    }

    /**
     * Resolve a single dependency
     */
    private function _resolveDependency(\ReflectionParameter $parameter)
    {
        $type = $parameter->getType();

        // Handle builtin types and non-typed parameters
        if ($type === null || !$type instanceof \ReflectionNamedType || $type->isBuiltin()) {
            if ($parameter->isDefaultValueAvailable()) {
                return $parameter->getDefaultValue();
            }

            throw new \RuntimeException(
                "Cannot resolve parameter {$parameter->getName()}"
            );
        }

        $typeName = $type->getName();

        // Try to resolve from container
        if (isset($this[$typeName])) {
            return $this[$typeName];
        }

        // Try to resolve as interface/class
        try {
            return $this->make($typeName);
        } catch (\Exception $e) {
            if ($parameter->isDefaultValueAvailable()) {
                return $parameter->getDefaultValue();
            }

            if ($parameter->allowsNull()) {
                return null;
            }

            throw $e;
        }
    }
}
