<?php
/**
 * Plugin Kernel
 *
 * Lightweight kernel that manages the plugin lifecycle, detects context
 * (admin vs frontend), and provides a single Plugin instance.
 *
 * @package WP_PowerSuite\Bootstrap
 * @since 1.0.0
 */

namespace WP_PowerSuite\Bootstrap;

use WP_PowerSuite\Core\ModuleLifecycleListener;
use WP_PowerSuite\Core\Plugin;

/**
 * Kernel Class
 *
 * Manages plugin initialization, context detection, and provides
 * a single Plugin instance. Does NOT load modules.
 *
 * @since 1.0.0
 */
class Kernel {

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

	/**
	 * Plugin Instance
	 *
	 * The main Plugin instance managed by the kernel.
	 *
	 * @var Plugin|null
	 * @since 1.0.0
	 */
	private ?Plugin $plugin = null;

	/**
	 * Context Type
	 *
	 * Detected context: 'admin', 'frontend', or 'ajax'.
	 *
	 * @var string
	 * @since 1.0.0
	 */
	private string $context = '';

	/**
	 * Initialization Flag
	 *
	 * Tracks whether the kernel has finished bootstrapping (modules loaded).
	 *
	 * @var bool
	 * @since 1.0.0
	 */
	private bool $initialized = false;

	/**
	 * Get Kernel Instance
	 *
	 * Returns the singleton instance of the Kernel class.
	 *
	 * @since 1.0.0
	 * @return Kernel Kernel instance.
	 */
	public static function getInstance(): Kernel {
		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() {
	}

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

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

	/**
	 * Load Plugin Textdomain
	 *
	 * Loads MO files as early as possible on plugins_loaded (before translated strings) and
	 * again on init once WordPress has finalized the locale.
	 *
	 * @since 1.0.0
	 * @param bool $force_reload Unload and reload strings (e.g. after locale is finalized on init).
	 * @return void
	 */
	public function loadTextdomain( bool $force_reload = false ): void {
		$domain = 'wp-powersuite';
		if ( ! $force_reload && is_textdomain_loaded( $domain ) ) {
			return;
		}
		if ( $force_reload && is_textdomain_loaded( $domain ) ) {
			unload_textdomain( $domain );
		}
		$locale = apply_filters( 'plugin_locale', determine_locale(), $domain );

		$mofile = WP_POWERSUITE_PLUGIN_DIR . 'languages' . DIRECTORY_SEPARATOR . $domain . '-' . $locale . '.mo';

		if ( file_exists( $mofile ) ) {
			load_textdomain( $domain, $mofile );
		}

		load_plugin_textdomain(
			$domain,
			false,
			dirname( WP_POWERSUITE_PLUGIN_BASENAME ) . '/languages'
		);
	}

	/**
	 * Load MO files on init once WordPress has finalized the locale.
	 *
	 * @since 1.0.0
	 * @return void
	 */
	public function loadTextdomainOnInit(): void {
		$this->loadTextdomain( true );
	}

	/**
	 * Initialize Kernel
	 *
	 * Initializes the kernel, detects context, and sets up the Plugin instance.
	 * This method should be called once during the 'plugins_loaded' hook.
	 *
	 * @since 1.0.0
	 * @return void
	 * @throws \Exception If initialization fails.
	 */
	public function init(): void {
		if ( $this->initialized ) {
			return;
		}

		if ( ! function_exists( 'add_action' ) ) {
			throw new \Exception( 'WordPress functions not available. Plugin must be loaded within WordPress context.' );
		}

		add_action( 'plugins_loaded', [ \WP_PowerSuite\Core\PluginUpgrader::class, 'maybe_run' ], 6 );

		add_action( 'init', [ $this, 'loadTextdomainOnInit' ], 0 );
		add_action( 'init', [ \WP_PowerSuite\Core\OptionAutoloadMigration::class, 'maybe_run' ], 0 );

		// Context must be known before ModuleManager decides what to load.
		$this->detectContext();

		$this->plugin = Plugin::getInstance();
		$this->plugin->init();

		// Modules must register on plugins_loaded (not deferred to init): Turnstile and other
		// integrations hook authenticate / form handlers that run before or during early init.
		$this->initModuleManager();

		ModuleLifecycleListener::init();

		if ( class_exists( '\WP_PowerSuite\OAuth\OAuthServiceProvider' ) ) {
			\WP_PowerSuite\OAuth\OAuthServiceProvider::register();
		}

		/**
		 * Fires after core modules are registered; platform integrations hook here.
		 *
		 * @since 1.0.0
		 * @param Kernel $kernel Kernel instance.
		 * @param Plugin $plugin Plugin instance.
		 */
		do_action( 'wp_powersuite_boot', $this, $this->plugin );

		if ( class_exists( 'WP_PowerSuite\Core\WhiteLabel' ) ) {
			\WP_PowerSuite\Core\WhiteLabel::init();
		}

		if ( $this->isAdmin() || $this->isAjax() ) {
			if ( class_exists( 'WP_PowerSuite\Core\Ajax' ) ) {
				\WP_PowerSuite\Core\Ajax::getInstance()->registerRouter();
			}

			// Lifecycle AJAX (deactivate feedback, delete preference) must register on
			// plugins_loaded — not deferred to init — so admin-ajax.php can dispatch it.
			if ( class_exists( 'WP_PowerSuite\Core\UninstallHandler' ) ) {
				\WP_PowerSuite\Core\UninstallHandler::registerAjaxHandlers();
			}
		}

		if ( $this->isAdmin() && ! $this->isAjax() ) {
			$this->initAdmin();

			if ( class_exists( 'WP_PowerSuite\Core\UninstallHandler' ) ) {
				\WP_PowerSuite\Core\UninstallHandler::init();
			}

			if ( class_exists( 'WP_PowerSuite\Core\AdminNotices' ) ) {
				\WP_PowerSuite\Core\AdminNotices::init();
			}
		}

		$this->initialized = true;

		/**
		 * Fires after WP PowerSuite kernel has been initialized (modules loaded).
		 *
		 * @since 1.0.0
		 * @param Kernel $kernel Kernel instance.
		 * @param Plugin $plugin Plugin instance.
		 */
		do_action( 'wp_powersuite_kernel_initialized', $this, $this->plugin );
	}

	/**
	 * Initialize Module Manager
	 *
	 * Initializes the ModuleManager to load enabled modules.
	 * Called from {@see self::init()} on plugins_loaded.
	 *
	 * @since 1.0.0
	 * @return void
	 */
	private function initModuleManager(): void {
		if ( ! class_exists( 'WP_PowerSuite\Core\ModuleManager' ) ) {
			return;
		}

		try {
			$module_manager = \WP_PowerSuite\Core\ModuleManager::getInstance();
			if ( ! $module_manager->isInitialized() ) {
				$module_manager->init();
			}
			if ( method_exists( $this->plugin, 'setComponent' ) ) {
				$this->plugin->setComponent( 'module_manager', $module_manager );
			}
		} catch ( \Exception $e ) {
			// Fail soft: a ModuleManager error must not take down the whole plugin.
			error_log( 'WP PowerSuite: Failed to initialize ModuleManager: ' . $e->getMessage() );
		}
	}

	/**
	 * Detect Context
	 *
	 * Detects whether we're in admin, frontend, or AJAX context.
	 *
	 * @since 1.0.0
	 * @return void
	 */
	private function detectContext(): void {
		// AJAX before is_admin(): admin-ajax.php reports both.
		if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) {
			$this->context = 'ajax';
			return;
		}

		// WP-CLI: treat as admin so admin modules (e.g. AI Alt Text WP-CLI) load.
		if ( defined( 'WP_CLI' ) && WP_CLI ) {
			$this->context = 'admin';
			return;
		}

		if ( is_admin() ) {
			$this->context = 'admin';
			return;
		}

		// wp-login.php (is_admin() is false here; login hooks run in this runtime).
		if ( $this->isLoginRequest() ) {
			$this->context = 'login';
			return;
		}

		if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) {
			$this->context = 'rest';
			return;
		}

		if ( defined( 'DOING_CRON' ) && DOING_CRON ) {
			$this->context = 'cron';
			return;
		}

		$this->context = 'frontend';
	}

	/**
	 * Whether the current HTTP request is wp-login.php.
	 */
	private function isLoginRequest(): bool {
		if ( isset( $GLOBALS['pagenow'] ) && $GLOBALS['pagenow'] === 'wp-login.php' ) {
			return true;
		}

		if ( ! isset( $_SERVER['SCRIPT_NAME'] ) ) {
			return false;
		}

		$script = wp_normalize_path( sanitize_text_field( wp_unslash( $_SERVER['SCRIPT_NAME'] ) ) );

		return str_ends_with( $script, '/wp-login.php' );
	}

	/**
	 * Get Plugin Instance
	 *
	 * Returns the main Plugin instance managed by the kernel.
	 *
	 * @since 1.0.0
	 * @return Plugin Plugin instance.
	 * @throws \Exception If kernel is not initialized.
	 */
	public function getPlugin(): Plugin {
		if ( $this->plugin === null ) {
			throw new \Exception( 'Kernel not initialized. Call init() first.' );
		}
		return $this->plugin;
	}

	/**
	 * Get Context
	 *
	 * Returns the detected context type.
	 *
	 * @since 1.0.0
	 * @return string Context type: 'admin', 'frontend', 'login', 'ajax', 'rest', or 'cron'.
	 */
	public function getContext(): string {
		return $this->context;
	}

	/**
	 * Is Admin Context
	 *
	 * Checks if we're in admin context.
	 *
	 * @since 1.0.0
	 * @return bool True if in admin context, false otherwise.
	 */
	public function isAdmin(): bool {
		return $this->context === 'admin';
	}

	/**
	 * Is Frontend Context
	 *
	 * Checks if we're in frontend context.
	 *
	 * @since 1.0.0
	 * @return bool True if in frontend context, false otherwise.
	 */
	public function isFrontend(): bool {
		return $this->context === 'frontend';
	}

	/**
	 * Is AJAX Context
	 *
	 * Checks if we're in AJAX context.
	 *
	 * @since 1.0.0
	 * @return bool True if in AJAX context, false otherwise.
	 */
	public function isAjax(): bool {
		return $this->context === 'ajax';
	}

	/**
	 * Whether the current HTTP request is wp-admin/admin-post.php.
	 *
	 * admin-post.php runs admin_init and admin_post_{action} but not admin_menu, so
	 * modules deferred to admin_menu must boot immediately on these requests.
	 *
	 * @since 2.0.1
	 */
	public function isAdminPost(): bool {
		if ( isset( $GLOBALS['pagenow'] ) && $GLOBALS['pagenow'] === 'admin-post.php' ) {
			return true;
		}

		if ( ! isset( $_SERVER['SCRIPT_NAME'] ) ) {
			return false;
		}

		$script = wp_normalize_path( sanitize_text_field( wp_unslash( $_SERVER['SCRIPT_NAME'] ) ) );

		return str_ends_with( $script, '/wp-admin/admin-post.php' );
	}

	/**
	 * Is REST Context
	 *
	 * Checks if we're in REST API context.
	 *
	 * @since 1.0.0
	 * @return bool True if in REST context, false otherwise.
	 */
	public function isRest(): bool {
		return $this->context === 'rest';
	}

	/**
	 * Is Cron Context
	 *
	 * Checks if we're in cron context.
	 *
	 * @since 1.0.0
	 * @return bool True if in cron context, false otherwise.
	 */
	public function isCron(): bool {
		return $this->context === 'cron';
	}

	/**
	 * Initialize Admin Components
	 *
	 * Initializes admin-specific components when in admin context.
	 *
	 * @since 1.0.0
	 * @return void
	 */
	private function initAdmin(): void {
		if ( class_exists( 'WP_PowerSuite\Core\AdminMenu' ) ) {
			\WP_PowerSuite\Core\AdminMenu::getInstance()->init();
		}

		if ( class_exists( 'WP_PowerSuite\Core\Assets' ) ) {
			\WP_PowerSuite\Core\Assets::getInstance()->init();
		}
	}

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

