<?php
/**
 * Service Container
 *
 * Lightweight dependency injection container for managing service instances.
 *
 * @package Sequensy
 * @since 2.0.0
 */

declare(strict_types=1);

namespace Sequensy;

use InvalidArgumentException;

// If this file is called directly, abort.
if ( ! defined( 'WPINC' ) ) {
	die;
}

/**
 * Class Container
 *
 * Simple service container that provides:
 * - Service registration via factory callables
 * - Lazy instantiation (services created on first access)
 * - Singleton instances (same instance returned on subsequent gets)
 * - Global accessor via singleton pattern
 */
class Container {
	/**
	 * Registered service factories.
	 *
	 * @var array<string, callable>
	 */
	private array $factories = array();

	/**
	 * Resolved service instances.
	 *
	 * @var array<string, object>
	 */
	private array $instances = array();

	/**
	 * Global container instance.
	 *
	 * @var Container|null
	 */
	private static ?Container $instance = null;

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

	/**
	 * Set the global container instance.
	 *
	 * Useful for testing to inject a mock container.
	 *
	 * @param Container|null $container Container instance or null to reset.
	 */
	public static function setInstance( ?Container $container ): void {
		self::$instance = $container;
	}

	/**
	 * Register a service factory.
	 *
	 * The factory callable receives the container as its only argument,
	 * allowing services to resolve their own dependencies.
	 *
	 * @param string   $name    Service identifier.
	 * @param callable $factory Factory function that returns the service instance.
	 * @return self For method chaining.
	 */
	public function register( string $name, callable $factory ): self {
		$this->factories[ $name ] = $factory;
		// Clear any existing instance so it will be recreated with new factory.
		unset( $this->instances[ $name ] );
		return $this;
	}

	/**
	 * Register a pre-created instance directly.
	 *
	 * Useful for registering existing objects or primitives.
	 *
	 * @param string $name     Service identifier.
	 * @param mixed  $instance The instance to register.
	 * @return self For method chaining.
	 */
	public function instance( string $name, mixed $instance ): self {
		$this->instances[ $name ] = $instance;
		return $this;
	}

	/**
	 * Get a service instance.
	 *
	 * Creates the instance on first access using the registered factory.
	 * Returns the same instance on subsequent calls (singleton behavior).
	 *
	 * @param string $name Service identifier.
	 * @return mixed The service instance.
	 * @throws InvalidArgumentException If service is not registered.
	 */
	public function get( string $name ): mixed {
		// Return cached instance if available.
		if ( isset( $this->instances[ $name ] ) ) {
			return $this->instances[ $name ];
		}

		// Check if factory is registered.
		if ( ! isset( $this->factories[ $name ] ) ) {
			// phpcs:disable WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Exception message.
			throw new InvalidArgumentException(
				sprintf( 'Service "%s" is not registered in the container.', $name )
			);
			// phpcs:enable WordPress.Security.EscapeOutput.ExceptionNotEscaped
		}

		// Create instance using factory.
		$this->instances[ $name ] = call_user_func( $this->factories[ $name ], $this );

		return $this->instances[ $name ];
	}

	/**
	 * Check if a service is registered.
	 *
	 * @param string $name Service identifier.
	 * @return bool True if registered, false otherwise.
	 */
	public function has( string $name ): bool {
		return isset( $this->factories[ $name ] ) || isset( $this->instances[ $name ] );
	}

	/**
	 * Remove a service registration and cached instance.
	 *
	 * @param string $name Service identifier.
	 * @return self For method chaining.
	 */
	public function remove( string $name ): self {
		unset( $this->factories[ $name ] );
		unset( $this->instances[ $name ] );
		return $this;
	}

	/**
	 * Clear all registrations and instances.
	 *
	 * Useful for testing to reset container state.
	 *
	 * @return self For method chaining.
	 */
	public function clear(): self {
		$this->factories = array();
		$this->instances = array();
		return $this;
	}

	/**
	 * Get all registered service names.
	 *
	 * @return array<string> List of service names.
	 */
	public function getRegisteredServices(): array {
		return array_unique(
			array_merge(
				array_keys( $this->factories ),
				array_keys( $this->instances )
			)
		);
	}
}
