<?php
/**
 * Module Manager
 *
 * Manages the loading, initialization, and lifecycle of WP PowerSuite modules.
 * Ensures only enabled modules are loaded and respects context requirements
 * (admin vs frontend). Designed to scale to 500+ modules efficiently.
 *
 * @package WP_PowerSuite\Core
 * @since 1.0.0
 */

namespace WP_PowerSuite\Core;

use WP_PowerSuite\Bootstrap\Kernel;
use WP_PowerSuite\Core\ConflictDetector;
use WP_PowerSuite\Core\AbstractModule;
use WP_PowerSuite\Core\Contracts\ModuleInterface;
use WP_PowerSuite\Core\ModuleRegistry;
use WP_PowerSuite\Core\OptionStore;
use WP_PowerSuite\Core\PerformanceBudgetConfig;
use WP_PowerSuite\Core\PerformanceMonitor;
use WP_PowerSuite\Core\HookTracker;

/**
 * Module Manager Class
 *
 * Handles module discovery, loading, initialization, and context-aware
 * activation. Modules are loaded lazily and only when enabled and in the
 * appropriate context.
 *
 * @since 1.0.0
 */
class ModuleManager {

	/**
	 * Module Manager Instance
	 *
	 * Singleton instance of the ModuleManager class.
	 *
	 * @var ModuleManager|null
	 * @since 1.0.0
	 */
	private static ?ModuleManager $instance = null;

	/**
	 * Loaded Modules
	 *
	 * Array of loaded module instances, keyed by module ID.
	 *
	 * @var array<string, ModuleInterface>
	 * @since 1.0.0
	 */
	private array $loaded_modules = [];

	/**
	 * Fully initialized module instances, keyed by module ID.
	 *
	 * @var array<string, ModuleInterface>
	 */
	private array $initialized_modules = [];

	/**
	 * When true, dependency modules must initialize immediately.
	 *
	 * @var bool
	 */
	private bool $resolving_dependencies = false;

	/**
	 * Module Registry
	 *
	 * Reference to the ModuleRegistry instance.
	 *
	 * @var ModuleRegistry|null
	 * @since 1.0.0
	 */
	private ?ModuleRegistry $module_registry = null;

	/**
	 * Enabled Modules Cache
	 *
	 * Cached list of enabled module IDs (request-level cache).
	 *
	 * @var array<string>|null
	 * @since 1.0.0
	 */
	private ?array $enabled_modules_cache = null;

	/**
	 * Initialization Flag
	 *
	 * Tracks whether the ModuleManager has been initialized.
	 *
	 * @var bool
	 * @since 1.0.0
	 */
	private bool $initialized = false;

	/**
	 * Kernel Instance
	 *
	 * Reference to the Kernel for context detection.
	 *
	 * @var Kernel|null
	 * @since 1.0.0
	 */
	private ?Kernel $kernel = null;

	/**
	 * Option Store Instance
	 *
	 * Reference to the OptionStore for settings management.
	 *
	 * @var OptionStore|null
	 * @since 1.0.0
	 */
	private ?OptionStore $option_store = null;

	/**
	 * Performance Monitor Instance
	 *
	 * Reference to the PerformanceMonitor for performance tracking.
	 *
	 * @var PerformanceMonitor|null
	 * @since 1.0.0
	 */
	private ?PerformanceMonitor $performance_monitor = null;

	/**
	 * Hook Tracker Instance
	 *
	 * Reference to the HookTracker for hook counting.
	 *
	 * @var HookTracker|null
	 * @since 1.0.0
	 */
	private ?HookTracker $hook_tracker = null;

	/**
	 * Maximum Modules Per Request
	 *
	 * Performance guardrail: maximum number of modules that can be
	 * loaded in a single request. Raised to support 80+ enabled modules.
	 *
	 * @var int
	 * @since 1.0.0
	 */
	private int $max_modules_per_request = 150;

	/**
	 * Module IDs skipped during the current request because the load cap was reached.
	 *
	 * @var array<string>
	 */
	private array $skipped_due_to_cap = [];

	public const MODULE_CAP_WARNING_TRANSIENT = 'wp_powersuite_module_cap_warning';

	/**
	 * Get Module Manager Instance
	 *
	 * Returns the singleton instance of the ModuleManager class.
	 *
	 * @since 1.0.0
	 * @return ModuleManager ModuleManager instance.
	 */
	public static function getInstance(): ModuleManager {
		if ( self::$instance === null ) {
			self::$instance = new self();
		}
		return self::$instance;
	}

	/**
	 * Constructor
	 *
	 * Private constructor to enforce singleton pattern.
	 *
	 * @since 1.0.0
	 */
	private function __construct() {
		// Constructor is private to prevent direct instantiation
	}

	/**
	 * Prevent Cloning
	 *
	 * Prevents cloning of the singleton instance.
	 *
	 * @since 1.0.0
	 * @return void
	 */
	private function __clone() {
		// Prevent cloning
	}

	/**
	 * Prevent Unserialization
	 *
	 * Prevents unserialization of the singleton instance.
	 *
	 * @since 1.0.0
	 * @return void
	 */
	public function __wakeup(): void {
		throw new \Exception( 'Cannot unserialize singleton' );
	}

	/**
	 * Initialize Module Manager
	 *
	 * Initializes the ModuleManager and loads enabled modules based on
	 * the current context. This should be called after the Kernel is
	 * initialized.
	 *
	 * @since 1.0.0
	 * @return void
	 * @throws \Exception If initialization fails.
	 */
	public function init(): void {
		// Prevent multiple initializations
		if ( $this->initialized ) {
			return;
		}

		$this->option_store = OptionStore::getInstance();
		$enabled_modules = $this->option_store->getEnabledModules();

		// If no modules are enabled, skip all initialization (zero overhead)
		if ( empty( $enabled_modules ) ) {
			// Only initialize admin menu if in admin (user needs access to enable modules)
			if ( is_admin() ) {
				// Minimal admin menu registration - no registry, no performance monitor needed
				$this->initialized = true;
				return;
			}
			// On frontend with no modules, do nothing
			$this->initialized = true;
			return;
		}

		try {
			$this->kernel = Kernel::getInstance();
		} catch ( \Exception $e ) {
			throw new \Exception( 'Kernel not initialized. ModuleManager requires Kernel to be initialized first.' );
		}

		// Verify Kernel exists and has context (more lenient check)
		// We check for context instead of isInitialized() to avoid circular dependency
		// The Kernel will be fully initialized after ModuleManager is set up
		if ( ! $this->kernel || empty( $this->kernel->getContext() ) ) {
			throw new \Exception( 'Kernel not ready. ModuleManager requires Kernel context to be set.' );
		}

		$this->module_registry = $this->loadModuleRegistry();
		if ( ! $this->module_registry->isInitialized() ) {
			$this->module_registry->init();
		}

		if ( PerformanceBudgetConfig::isPerformanceMonitorEnabled() && class_exists( PerformanceMonitor::class ) ) {
			$this->performance_monitor = PerformanceMonitor::getInstance();
			$this->hook_tracker        = HookTracker::getInstance();
		}

		// Allow themes/plugins to raise or lower the module load cap
		$this->max_modules_per_request = max( 1, (int) apply_filters( 'wp_powersuite_max_modules_per_request', $this->max_modules_per_request ) );

		$this->loadEnabledModules();

		// Mark as initialized
		$this->initialized = true;

		/**
		 * Fires after ModuleManager has been initialized and modules loaded.
		 *
		 * @since 1.0.0
		 * @param ModuleManager $manager ModuleManager instance.
		 * @param array<string, ModuleInterface> $loaded_modules Loaded module instances.
		 */
		do_action( 'wp_powersuite_modules_loaded', $this, $this->initialized_modules );
	}

	/**
	 * Load Enabled Modules
	 *
	 * Loads all enabled modules that are appropriate for the current context.
	 * Admin-only modules are never loaded on frontend, and frontend modules
	 * are never loaded in admin unless explicitly required.
	 *
	 * @since 1.0.0
	 * @return void
	 */
	private function loadEnabledModules(): void {
		$enabled_module_ids = $this->getEnabledModules();

		// If no modules enabled, return early
		if ( empty( $enabled_module_ids ) ) {
			return;
		}

		// Stable order so the same set of modules always loads the same way (no "sometimes works").
		sort( $enabled_module_ids, SORT_STRING );

		$current_context = $this->kernel->getContext();

		// License check: premium modules require valid license to load
		$license_valid = wp_powersuite_is_license_valid();

		$skipped_due_to_cap = [];

		foreach ( $enabled_module_ids as $module_id ) {
			// Skip premium modules when license is invalid (security-critical modules keep running).
			if ( ! $license_valid && ! SecurityCriticalModules::isLicenseExempt( $module_id ) ) {
				$meta = $this->getModuleMetadata( $module_id );
				if ( ! empty( $meta['is_pro'] ) ) {
					continue;
				}
			}

			// Check if we've exceeded the maximum modules limit (security-critical modules are exempt).
			if (
				count( $this->loaded_modules ) >= $this->max_modules_per_request
				&& ! SecurityCriticalModules::isLoadCapExempt( $module_id )
			) {
				error_log(
					sprintf(
						'WP PowerSuite: Maximum modules per request (%d) exceeded. Module "%s" not loaded.',
						$this->max_modules_per_request,
						$module_id
					)
				);
				$skipped_due_to_cap[] = $module_id;
				continue;
			}

			if ( ! $this->shouldLoadModule( $module_id, $current_context ) ) {
				continue;
			}

			try {
				$this->loadModule( $module_id );
			} catch ( \Exception $e ) {
				// Log error but continue loading other modules
				error_log(
					sprintf(
						'WP PowerSuite: Failed to load module "%s": %s',
						$module_id,
						$e->getMessage()
					)
				);
			}
		}

		if ( $skipped_due_to_cap !== [] ) {
			$this->skipped_due_to_cap = $skipped_due_to_cap;
			set_transient(
				self::MODULE_CAP_WARNING_TRANSIENT,
				[
					'modules' => $skipped_due_to_cap,
					'cap'     => $this->max_modules_per_request,
				],
				DAY_IN_SECONDS
			);
		}
	}

	/**
	 * Load enabled modules for a specific context (e.g. login on a masked login URL).
	 *
	 * @since 1.0.0
	 */
	public function loadEnabledModulesForContext( string $context ): void {
		$context = sanitize_key( $context );
		if ( $context === '' ) {
			return;
		}

		$enabled_module_ids = $this->getEnabledModules();
		if ( $enabled_module_ids === [] ) {
			return;
		}

		sort( $enabled_module_ids, SORT_STRING );

		$license_valid = wp_powersuite_is_license_valid();

		foreach ( $enabled_module_ids as $module_id ) {
			if ( isset( $this->loaded_modules[ $module_id ] ) ) {
				continue;
			}

			if ( ! $license_valid && ! SecurityCriticalModules::isLicenseExempt( $module_id ) ) {
				$meta = $this->getModuleMetadata( $module_id );
				if ( ! empty( $meta['is_pro'] ) ) {
					continue;
				}
			}

			if (
				count( $this->loaded_modules ) >= $this->max_modules_per_request
				&& ! SecurityCriticalModules::isLoadCapExempt( $module_id )
			) {
				break;
			}

			if ( ! $this->shouldLoadModule( $module_id, $context ) ) {
				continue;
			}

			try {
				$this->loadModule( $module_id );
			} catch ( \Exception $e ) {
				error_log(
					sprintf(
						'WP PowerSuite: Failed to load module "%s" for context "%s": %s',
						$module_id,
						$context,
						$e->getMessage()
					)
				);
			}
		}
	}

	/**
	 * Check if Module Should Be Loaded
	 *
	 * Determines if a module should be loaded based on the current context
	 * and the module's context requirements.
	 *
	 * @since 1.0.0
	 * @param string $module_id Module ID to check.
	 * @param string $current_context Current context (admin, frontend, ajax, etc.).
	 * @return bool True if module should be loaded, false otherwise.
	 */
	private function shouldLoadModule( string $module_id, string $current_context ): bool {
		$module_metadata = $this->getModuleMetadata( $module_id );

		if ( empty( $module_metadata ) ) {
			return false;
		}

		$module_context = $module_metadata['context'] ?? [ 'both' ];
		if ( ! is_array( $module_context ) ) {
			$module_context = [ $module_context ];
		}
		if ( $module_context === [] ) {
			$module_context = [ 'both' ];
		}

		if (
			$module_id === 'avif-conversion'
			&& $current_context === 'frontend'
			&& in_array( 'frontend', $module_context, true )
		) {
			$settings = OptionStoreModuleSettingsRepository::forModule( 'avif-conversion' )->all( [] );
			if ( ! is_array( $settings ) || empty( $settings['serve_avif_picture'] ) ) {
				return false;
			}
		}

		return $this->isContextMatch( $current_context, $module_context );
	}

	/**
	 * Check if Context Matches
	 *
	 * Determines if the current context matches the module's context requirements.
	 *
	 * @since 1.0.0
	 * @param string $current_context Current context.
	 * @param array<string> $module_contexts Module's required contexts.
	 * @return bool True if context matches, false otherwise.
	 */
	private function isContextMatch( string $current_context, array $module_contexts ): bool {
		// Context mapping for smart matching
		$context_map = [
			'admin'    => [ 'admin', 'both' ],
			'frontend' => [ 'frontend', 'both' ],
			'login'    => [ 'login', 'frontend', 'both' ],
			'ajax'     => [ 'ajax', 'both' ],
			'rest'     => [ 'rest', 'both' ],
			'cron'     => [ 'cron', 'both' ],
		];

		if ( isset( $context_map[ $current_context ] ) ) {
			$allowed_contexts = $context_map[ $current_context ];
			foreach ( $module_contexts as $context ) {
				if ( in_array( $context, $allowed_contexts, true ) ) {
					return true;
				}
			}
		}

		// Direct context match
		return in_array( $current_context, $module_contexts, true );
	}

	/**
	 * Load Module
	 *
	 * Loads and initializes a single module by ID. Verifies the module
	 * is enabled and implements ModuleInterface before loading.
	 *
	 * @since 1.0.0
	 * @param string $module_id Module ID to load.
	 * @return ModuleInterface Loaded module instance.
	 * @throws \Exception If module cannot be loaded.
	 */
	public function loadModule( string $module_id ): ModuleInterface {
		if ( isset( $this->loaded_modules[ $module_id ] ) ) {
			return $this->loaded_modules[ $module_id ];
		}

		if ( ! $this->isModuleEnabled( $module_id ) ) {
			throw new \Exception( esc_html( sprintf( 'Module "%s" is not enabled.', $module_id ) ) );
		}

		$conflict_detector = ConflictDetector::getInstance();
		$enabled_ids = $this->getEnabledModules();
		$blocking = $conflict_detector->getBlockingConflictsForEnable( $module_id, $enabled_ids );
		if ( ! empty( $blocking ) ) {
			throw new \Exception(
				esc_html(
					sprintf(
						'Module "%s" conflicts with enabled module(s): %s. Disable the conflicting module(s) first.',
						$module_id,
						implode( ', ', $blocking )
					)
				)
			);
		}

		if ( $this->performance_monitor && $this->performance_monitor->isModuleDisabled( $module_id ) ) {
			$error_message = $this->performance_monitor->getDisabledModuleError( $module_id );
			throw new \Exception(
				esc_html(
					sprintf(
						'Module "%s" has been auto-disabled due to a fatal error: %s',
						$module_id,
						$error_message ?? 'Unknown error'
					)
				)
			);
		}

		$module_metadata = $this->getModuleMetadata( $module_id );

		if ( empty( $module_metadata ) || ! isset( $module_metadata['class'] ) ) {
			throw new \Exception( esc_html( sprintf( 'Module "%s" not found in registry.', $module_id ) ) );
		}

		$this->assertModuleRequirements( $module_metadata );

		$module_class = $module_metadata['class'];
		if ( ! class_exists( $module_class ) ) {
			// Try to require the module file
			if ( isset( $module_metadata['_directory'] ) && ! empty( $module_metadata['_directory'] ) ) {
				// Try both Module.php (capital M) and module.php (lowercase m)
				$module_file_capital = $module_metadata['_directory'] . DIRECTORY_SEPARATOR . 'Module.php';
				$module_file_lower   = $module_metadata['_directory'] . DIRECTORY_SEPARATOR . 'module.php';
				
				if ( file_exists( $module_file_capital ) && is_readable( $module_file_capital ) ) {
					require_once $module_file_capital;
				} elseif ( file_exists( $module_file_lower ) && is_readable( $module_file_lower ) ) {
					require_once $module_file_lower;
				}
			}

			if ( ! class_exists( $module_class ) ) {
				throw new \Exception( esc_html( sprintf( 'Module class "%s" not found.', $module_class ) ) );
			}
		}

		// Instantiate module
		$module = new $module_class();

		// Verify it implements ModuleInterface
		if ( ! $module instanceof ModuleInterface ) {
			throw new \Exception(
				esc_html( sprintf( 'Module "%s" does not implement ModuleInterface.', $module_id ) )
			);
		}

		// Verify module ID matches
		if ( $module->getId() !== $module_id ) {
			throw new \Exception(
				esc_html(
					sprintf(
						'Module ID mismatch. Expected "%s", got "%s".',
						$module_id,
						$module->getId()
					)
				)
			);
		}

		$dependencies = $module->getDependencies();
		if ( ! empty( $dependencies ) ) {
			$this->resolving_dependencies = true;
			try {
				foreach ( $dependencies as $dependency_id ) {
					if ( isset( $this->initialized_modules[ $dependency_id ] ) ) {
						continue;
					}

					try {
						$this->loadModule( $dependency_id );
					} catch ( \Exception $e ) {
						throw new \Exception(
							esc_html(
								sprintf(
									'Module "%s" depends on "%s", which could not be loaded: %s',
									$module_id,
									$dependency_id,
									$e->getMessage()
								)
							)
						);
					}

					if ( ! isset( $this->initialized_modules[ $dependency_id ] ) ) {
						throw new \Exception(
							esc_html(
								sprintf(
									'Module "%s" depends on "%s", which did not finish initialization.',
									$module_id,
									$dependency_id
								)
							)
						);
					}
				}
			} finally {
				$this->resolving_dependencies = false;
			}
		}

		// Start performance monitoring and initialize (immediate or deferred by boot tier).
		$boot_tier = isset( $module_metadata['boot'] ) && is_string( $module_metadata['boot'] )
			? $module_metadata['boot']
			: 'critical';

		$perform_init = function () use ( $module, $module_id ): void {
			if ( $this->performance_monitor ) {
				$this->performance_monitor->startModuleMonitoring( $module_id );
			}

			if ( $this->hook_tracker ) {
				$this->hook_tracker->takeSnapshot( $module_id );
			}

			try {
				$module->init();
				if ( $module instanceof AbstractModule && $module->isEnabled() && ! $module->isInitialized() ) {
					throw new \Exception( esc_html( sprintf( 'Module "%s" did not complete initialization.', $module_id ) ) );
				}
			} catch ( \Throwable $e ) {
				if ( $this->performance_monitor ) {
					$this->performance_monitor->handleModuleError( $module_id, $e );
				}

				$init_error = $e->getMessage();
				throw new \Exception(
					esc_html(
						sprintf(
							'Failed to initialize module "%s": %s',
							$module_id,
							$init_error
						)
					)
				);
			}

			if ( $this->hook_tracker && $this->performance_monitor ) {
				$hook_count = $this->hook_tracker->getHookCountDelta( $module_id );
				for ( $i = 0; $i < $hook_count; $i++ ) {
					$this->performance_monitor->incrementHookCount( $module_id );
				}
			}

			if ( $this->performance_monitor ) {
				$this->performance_monitor->endModuleMonitoring( $module_id );
			}

			do_action( 'wp_powersuite_module_loaded', $module, $module_id );
			$this->initialized_modules[ $module_id ] = $module;
		};

		$this->loaded_modules[ $module_id ] = $module;

		$initialized_immediately = ModuleBootScheduler::getInstance()->schedule(
			$module_id,
			$boot_tier,
			$perform_init,
			$this->resolving_dependencies
		);

		if ( $initialized_immediately && $module instanceof AbstractModule && $module->isEnabled() && ! $module->isInitialized() ) {
			unset( $this->loaded_modules[ $module_id ], $this->initialized_modules[ $module_id ] );
			throw new \Exception( esc_html( sprintf( 'Module "%s" did not complete initialization.', $module_id ) ) );
		}

		return $module;
	}

	/**
	 * Get Enabled Modules
	 *
	 * Returns an array of enabled module IDs. Results are cached at
	 * the request level for performance.
	 *
	 * @since 1.0.0
	 * @return array<string> Array of enabled module IDs.
	 */
	public function getEnabledModules(): array {
		if ( $this->enabled_modules_cache !== null ) {
			return $this->enabled_modules_cache;
		}

		if ( $this->option_store === null ) {
			$this->option_store = OptionStore::getInstance();
		}

		$enabled_modules = $this->option_store->getEnabledModules();

		// Cache for this request
		$this->enabled_modules_cache = $enabled_modules;

		return $this->enabled_modules_cache;
	}

	/**
	 * Check if Module is Enabled
	 *
	 * Checks if a specific module is enabled. Uses cached enabled modules
	 * list for performance.
	 *
	 * @since 1.0.0
	 * @param string $module_id Module ID to check.
	 * @return bool True if enabled, false otherwise.
	 */
	public function isModuleEnabled( string $module_id ): bool {
		if ( $this->option_store === null ) {
			$this->option_store = OptionStore::getInstance();
		}

		return $this->option_store->isModuleEnabled( $module_id );
	}

	/**
	 * Check if Module is Loaded
	 *
	 * Checks if a module has completed initialization.
	 *
	 * @since 1.0.0
	 * @param string $module_id Module ID to check.
	 * @return bool True if loaded, false otherwise.
	 */
	public function isModuleLoaded( string $module_id ): bool {
		return isset( $this->initialized_modules[ $module_id ] );
	}

	/**
	 * Get Loaded Module
	 *
	 * Returns a fully initialized module instance by ID, or null when init
	 * has not completed (including deferred boot tiers).
	 *
	 * @since 1.0.0
	 * @param string $module_id Module ID.
	 * @return ModuleInterface|null Module instance or null if not initialized.
	 */
	public function getModule( string $module_id ): ?ModuleInterface {
		return $this->initialized_modules[ $module_id ] ?? null;
	}

	/**
	 * Get All Loaded Modules
	 *
	 * Returns all initialized module instances.
	 *
	 * @since 1.0.0
	 * @return array<string, ModuleInterface> Array of loaded modules, keyed by module ID.
	 */
	public function getLoadedModules(): array {
		return $this->initialized_modules;
	}

	/**
	 * Verify PHP and WordPress versions satisfy module metadata requirements.
	 *
	 * @param array<string, mixed> $metadata Module metadata from the registry.
	 * @throws \Exception When the runtime does not meet module requirements.
	 */
	private function assertModuleRequirements( array $metadata ): void {
		$requires = $metadata['requires'] ?? [];
		if ( ! is_array( $requires ) ) {
			return;
		}

		$module_id = isset( $metadata['id'] ) ? (string) $metadata['id'] : 'unknown';

		$required_php = isset( $requires['php'] ) ? (string) $requires['php'] : '';
		if ( $required_php !== '' && version_compare( PHP_VERSION, $required_php, '<' ) ) {
			throw new \Exception(
				esc_html(
					sprintf(
						'Module "%s" requires PHP %s or higher (running %s).',
						$module_id,
						$required_php,
						PHP_VERSION
					)
				)
			);
		}

		global $wp_version;
		$required_wp = isset( $requires['wordpress'] ) ? (string) $requires['wordpress'] : '';
		if ( $required_wp !== '' && is_string( $wp_version ) && version_compare( $wp_version, $required_wp, '<' ) ) {
			throw new \Exception(
				esc_html(
					sprintf(
						'Module "%s" requires WordPress %s or higher (running %s).',
						$module_id,
						$required_wp,
						$wp_version
					)
				)
			);
		}
	}

	/**
	 * Get Module Metadata
	 *
	 * Retrieves module metadata from the registry.
	 *
	 * @since 1.0.0
	 * @param string $module_id Module ID.
	 * @return array<string, mixed> Module metadata or empty array if not found.
	 */
	private function getModuleMetadata( string $module_id ): array {
		if ( $this->module_registry === null ) {
			return [];
		}

		$metadata = $this->module_registry->getModuleMetadata( $module_id );
		return $metadata ?? [];
	}

	/**
	 * Load ModuleRegistry from the same directory when the autoloader misses it.
	 *
	 * @since 1.0.8
	 * @throws \RuntimeException When the class file is missing or does not define the class.
	 */
	private function loadModuleRegistry(): ModuleRegistry {
		$this->requireSiblingClass( DataRegistry::class, 'DataRegistry.php' );
		$this->requireSiblingClass( ModuleRegistry::class, 'ModuleRegistry.php' );

		return ModuleRegistry::getInstance();
	}

	/**
	 * @param class-string $class    Fully qualified class name.
	 * @param string       $filename File in src/Core/.
	 * @throws \RuntimeException When the sibling file is missing or the class is still undefined.
	 */
	private function requireSiblingClass( string $class, string $filename ): void {
		if ( class_exists( $class, false ) ) {
			return;
		}

		$file = __DIR__ . DIRECTORY_SEPARATOR . $filename;
		if ( is_readable( $file ) ) {
			require_once $file;
		}

		if ( ! class_exists( $class, false ) ) {
			throw new \RuntimeException(
				esc_html(
					sprintf(
						'WP PowerSuite core file src/Core/%s is missing or unreadable. Reinstall the plugin.',
						$filename
					)
				)
			);
		}
	}

	/**
	 * Get Module Registry
	 *
	 * Returns the ModuleRegistry instance.
	 *
	 * @since 1.0.0
	 * @return ModuleRegistry|null ModuleRegistry instance or null if not initialized.
	 */
	public function getModuleRegistry(): ?ModuleRegistry {
		return $this->module_registry;
	}

	/**
	 * Set Maximum Modules Per Request
	 *
	 * Sets the maximum number of modules that can be loaded per request.
	 * This is a performance guardrail.
	 *
	 * @since 1.0.0
	 * @param int $max Maximum number of modules.
	 * @return void
	 */
	public function setMaxModulesPerRequest( int $max ): void {
		$this->max_modules_per_request = max( 1, $max );
	}

	/**
	 * Get Maximum Modules Per Request
	 *
	 * Returns the maximum number of modules that can be loaded per request.
	 *
	 * @since 1.0.0
	 * @return int Maximum number of modules.
	 */
	public function getMaxModulesPerRequest(): int {
		return $this->max_modules_per_request;
	}

	/**
	 * Module IDs skipped in the current request because the load cap was reached.
	 *
	 * @return array<string>
	 */
	public function getSkippedDueToCap(): array {
		return $this->skipped_due_to_cap;
	}

	/**
	 * Check if Module Manager is Initialized
	 *
	 * Returns whether the ModuleManager has been initialized.
	 *
	 * @since 1.0.0
	 * @return bool True if initialized, false otherwise.
	 */
	public function isInitialized(): bool {
		return $this->initialized;
	}

	/**
	 * Clear Enabled Modules Cache
	 *
	 * Clears the request-level cache of enabled modules. Useful when
	 * modules are enabled/disabled during the request.
	 *
	 * @since 1.0.0
	 * @return void
	 */
	public function clearEnabledModulesCache(): void {
		$this->enabled_modules_cache = null;

		// Also clear OptionStore cache
		if ( $this->option_store !== null ) {
			$this->option_store->clearCache();
		}
	}

	/**
	 * Get Option Store
	 *
	 * Returns the OptionStore instance.
	 *
	 * @since 1.0.0
	 * @return OptionStore|null OptionStore instance or null if not initialized.
	 */
	public function getOptionStore(): ?OptionStore {
		return $this->option_store;
	}
}

