<?php

namespace WPUltimateSecurity\UltimateSecurity\Infrastructure\Container;

use Exception;

/**
 * Service Container
 *
 * A PSR-11 compatible dependency injection container for the Ultimate Security plugin.
 * Supports singleton instances, factory bindings, and aliasing.
 *
 * Usage:
 * ```php
 * // Get the container instance
 * $container = Container::getInstance();
 *
 * // Register a singleton service
 * $container->singleton(SettingsInterface::class, function ($c) {
 *     return SettingsRepository::getInstance();
 * });
 *
 * // Register a factory binding (new instance each time)
 * $container->bind(Validator::class, function ($c) {
 *     return new RequestValidator($c->get(SettingsInterface::class));
 * });
 *
 * // Register an existing instance
 * $container->instance('config', $configArray);
 *
 * // Create an alias
 * $container->alias('settings', SettingsInterface::class);
 *
 * // Resolve a service
 * $settings = $container->get(SettingsInterface::class);
 * ```
 *
 * @package WPUltimateSecurity\UltimateSecurity\Infrastructure\Container
 * @since 2.0.0
 */
class Container implements ContainerInterface
{
    /**
     * The singleton instance of the container.
     *
     * @var self|null
     */
    private static ?self $instance = null;

    /**
     * Registered bindings.
     *
     * @var array<string, array{resolver: callable, shared: bool}>
     */
    private array $bindings = [];

    /**
     * Shared (singleton) instances.
     *
     * @var array<string, mixed>
     */
    private array $instances = [];

    /**
     * Aliases mapping to their abstract bindings.
     *
     * @var array<string, string>
     */
    private array $aliases = [];

    /**
     * Stack of services currently being resolved (for circular dependency detection).
     *
     * @var array<string>
     */
    private array $resolutionStack = [];

    /**
     * Private constructor to enforce singleton pattern.
     */
    private function __construct()
    {
        // Private constructor
    }

    /**
     * Prevent cloning.
     */
    private function __clone(): void
    {
        // Prevent cloning
    }

    /**
     * Get the singleton instance of the container.
     *
     * @return self
     */
    public static function getInstance(): self
    {
        if (self::$instance === null) {
            self::$instance = new self();
        }

        return self::$instance;
    }

    /**
     * Reset the container (primarily for testing).
     *
     * @return void
     */
    public static function reset(): void
    {
        self::$instance = null;
    }

    /**
     * {@inheritDoc}
     */
    public function get(string $id): mixed
    {
        // Resolve alias to actual binding
        $id = esc_html($this->resolveAlias($id));

        // Check if we have an existing shared instance
        if (isset($this->instances[$id])) {
            return $this->instances[$id];
        }

        // Check if we have a binding
        if (!isset($this->bindings[$id])) {
            throw new ServiceNotFoundException(esc_html($id));
        }

        // Detect circular dependencies
        if (in_array($id, $this->resolutionStack, true)) {
            throw ContainerException::forCircularDependency(esc_html($id), [...$this->resolutionStack, esc_html($id)]);
        }

        // Add to resolution stack
        $this->resolutionStack[] = $id;

        try {
            $binding = $this->bindings[$id];
            $resolved = ($binding['resolver'])($this);

            // If it's a shared binding, store the instance
            if ($binding['shared']) {
                $this->instances[$id] = $resolved;
            }

            return $resolved;
        } catch (Exception $e) {
            throw ContainerException::forResolutionError($id, $e);
        } finally {
            // Remove from resolution stack
            array_pop($this->resolutionStack);
        }
    }

    /**
     * {@inheritDoc}
     */
    public function has(string $id): bool
    {
        $id = $this->resolveAlias($id);

        return isset($this->instances[$id]) || isset($this->bindings[$id]);
    }

    /**
     * {@inheritDoc}
     */
    public function bind(string $id, callable $resolver, bool $shared = false): void
    {
        $this->bindings[$id] = [
            'resolver' => $resolver,
            'shared' => $shared,
        ];

        // Remove any existing instance if re-binding
        unset($this->instances[$id]);
    }

    /**
     * {@inheritDoc}
     */
    public function singleton(string $id, callable $resolver): void
    {
        $this->bind($id, $resolver, true);
    }

    /**
     * {@inheritDoc}
     */
    public function instance(string $id, mixed $instance): void
    {
        $this->instances[$id] = $instance;

        // Also create a binding that returns the instance
        $this->bindings[$id] = [
            'resolver' => fn() => $instance,
            'shared' => true,
        ];
    }

    /**
     * {@inheritDoc}
     */
    public function alias(string $alias, string $abstract): void
    {
        $this->aliases[$alias] = $abstract;
    }

    /**
     * Resolve an alias to its actual binding identifier.
     *
     * @param string $id The identifier (may be an alias).
     *
     * @return string The resolved binding identifier.
     */
    private function resolveAlias(string $id): string
    {
        while (isset($this->aliases[$id])) {
            $id = $this->aliases[$id];
        }

        return $id;
    }

    /**
     * Get all registered binding identifiers.
     *
     * @return array<string>
     */
    public function getBindings(): array
    {
        return array_keys($this->bindings);
    }

    /**
     * Get all registered aliases.
     *
     * @return array<string, string>
     */
    public function getAliases(): array
    {
        return $this->aliases;
    }

    /**
     * Check if a binding is registered as shared (singleton).
     *
     * @param string $id The binding identifier.
     *
     * @return bool
     */
    public function isShared(string $id): bool
    {
        $id = esc_html($this->resolveAlias($id));

        return isset($this->bindings[$id]) && $this->bindings[$id]['shared'];
    }

    /**
     * Create a new instance without using shared cache.
     *
     * Forces creation of a new instance even for singleton bindings.
     *
     * @param string $id The binding identifier.
     *
     * @return mixed
     * @throws ServiceNotFoundException If the binding doesn't exist.
     */
    public function make(string $id): mixed
    {
        $id = esc_html($this->resolveAlias($id));

        if (!isset($this->bindings[$id])) {
            throw new ServiceNotFoundException($id);
        }

        return ($this->bindings[$id]['resolver'])($this);
    }

    /**
     * Call a method on a resolved service, injecting dependencies.
     *
     * @param string $id     The service identifier.
     * @param string $method The method to call.
     * @param array  $params Additional parameters to pass.
     *
     * @return mixed The method return value.
     */
    public function call(string $id, string $method, array $params = []): mixed
    {
        $instance = $this->get($id);

        return $instance->$method(...$params);
    }
}
