<?php
declare(strict_types=1);

namespace Ekesto\CacheAutopilot;

use Ekesto\CacheInvalidator\Core\Lifecycle as InvalidatorLifecycle;
use Ekesto\CacheInvalidator\InvalidatorRuntime;
use Ekesto\CacheWarmup\Lifecycle as WarmupLifecycle;
use Ekesto\CacheWarmup\WarmupRuntime;

/**
 * Single-plugin orchestrator for Cache Autopilot (Phase 1).
 *
 * Boots both engines from one main plugin file while preserving their existing
 * namespaces, option names, cron hooks, REST namespaces, admin slugs, and the
 * `ekesto_ci_invalidation_urls` bridge action unchanged.
 *
 * Lifecycle ownership is centralized here: WordPress stores a single uninstall
 * callback per plugin file, so the orchestrator registers ONE activation,
 * deactivation, and uninstall hook and fans each out to both engines. Each
 * engine therefore exposes its lifecycle methods directly (activate/deactivate/
 * uninstall/maybeUpgrade) and does NOT self-register those hooks here.
 *
 * Engines do not share a build directory. Each engine is booted with a base
 * URL/path pointing at its own asset subfolder (`invalidator/`, `warmup/`),
 * so every internal runtime path/url reference resolves
 * into the correct subfolder with no engine-code changes.
 */
final class Bootstrap
{
    /** Asset/base subfolder for the Invalidator (decision) engine. */
    private const INVALIDATOR_SUBDIR = 'invalidator/';

    /** Asset/base subfolder for the Warmup (execution) engine. */
    private const WARMUP_SUBDIR = 'warmup/';

    /** WordPress plugin basename for the Free artifact. */
    private const FREE_PLUGIN_BASENAME = 'cache-autopilot/cache-autopilot.php';

    /** WordPress plugin basename for the PRO artifact. */
    private const PRO_PLUGIN_BASENAME = 'cache-autopilot-pro/cache-autopilot.php';

    /** Prefix used by WordPress for dynamic uninstall actions. */
    private const UNINSTALL_ACTION_PREFIX = 'uninstall_';

    /**
     * Wire the single lifecycle surface and boot both engines.
     *
     * @param string $pluginFile Absolute path to the main plugin file.
     * @return void
     */
    public static function init(string $pluginFile): void
    {
        register_activation_hook($pluginFile, [self::class, 'activate']);
        register_deactivation_hook($pluginFile, [self::class, 'deactivate']);
        register_uninstall_hook($pluginFile, [self::class, 'uninstall']);
        add_action('plugins_loaded', [self::class, 'maybeUpgrade'], 1);

        self::bootEngines($pluginFile);
    }

    /**
     * Legacy no-op retained for callers from older boot flows.
     *
     * @return void
     */
    public static function loadTextDomain(): void
    {
    }

    /**
     * Boot both engines, each scoped to its own asset subfolder.
     *
     * @param string $pluginFile Absolute path to the main plugin file.
     * @return void
     */
    private static function bootEngines(string $pluginFile): void
    {
        $baseUrl = plugin_dir_url($pluginFile);
        $basePath = plugin_dir_path($pluginFile);

        self::runGuarded(
            static function () use ($baseUrl, $basePath): void {
                if (!self::canBootEngine('Cache Invalidator', InvalidatorRuntime::bootRequiredClasses())) {
                    return;
                }

                InvalidatorRuntime::boot(
                    $baseUrl . self::INVALIDATOR_SUBDIR,
                    $basePath . self::INVALIDATOR_SUBDIR
                );
            },
            InvalidatorRuntime::class,
            'Invalidator boot'
        );

        self::runGuarded(
            static function () use ($baseUrl, $basePath): void {
                if (!self::canBootEngine('Cache Warmup', WarmupRuntime::bootRequiredClasses())) {
                    return;
                }

                WarmupRuntime::boot(
                    $baseUrl . self::WARMUP_SUBDIR,
                    $basePath . self::WARMUP_SUBDIR
                );
            },
            WarmupRuntime::class,
            'Warmup boot'
        );
    }

    /**
     * Activation fan-out: both engines.
     *
     * @return void
     */
    public static function activate(): void
    {
        self::runGuarded([InvalidatorLifecycle::class, 'activate'], InvalidatorLifecycle::class, 'Invalidator activate');
        self::runGuarded([WarmupLifecycle::class, 'activate'], WarmupLifecycle::class, 'Warmup activate');
    }

    /**
     * Deactivation fan-out: both engines.
     *
     * @return void
     */
    public static function deactivate(): void
    {
        self::runGuarded([InvalidatorLifecycle::class, 'deactivate'], InvalidatorLifecycle::class, 'Invalidator deactivate');
        self::runGuarded([WarmupLifecycle::class, 'deactivate'], WarmupLifecycle::class, 'Warmup deactivate');
    }

    /**
     * Uninstall fan-out: both engines.
     *
     * @return void
     */
    public static function uninstall(): void
    {
        if (self::shouldSkipUninstallCleanup()) {
            if (defined('WP_DEBUG') && WP_DEBUG) {
                // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Debug-only lifecycle guard logging before engine loggers are guaranteed available.
                error_log('Cache Autopilot uninstall cleanup skipped because the sibling edition is still installed.');
            }

            return;
        }

        self::runGuarded([InvalidatorLifecycle::class, 'uninstall'], InvalidatorLifecycle::class, 'Invalidator uninstall');
        self::runGuarded([WarmupLifecycle::class, 'uninstall'], WarmupLifecycle::class, 'Warmup uninstall');
    }

    /**
     * Idempotent upgrade reconciliation fan-out: both engines.
     *
     * @return void
     */
    public static function maybeUpgrade(): void
    {
        self::runGuarded([InvalidatorLifecycle::class, 'maybeUpgrade'], InvalidatorLifecycle::class, 'Invalidator maybeUpgrade');
        self::runGuarded([WarmupLifecycle::class, 'maybeUpgrade'], WarmupLifecycle::class, 'Warmup maybeUpgrade');
    }

    /**
     * Return whether uninstall cleanup must be preserved for an installed sibling.
     *
     * @return bool
     */
    private static function shouldSkipUninstallCleanup(): bool
    {
        $deletedPluginBasename = self::getDeletedPluginBasenameFromUninstallAction();
        if ($deletedPluginBasename === '') {
            return false;
        }

        $siblingPluginBasename = self::getSiblingPluginBasename($deletedPluginBasename);
        if ($siblingPluginBasename === '') {
            return false;
        }

        if (!defined('WP_PLUGIN_DIR')) {
            return false;
        }

        return file_exists(WP_PLUGIN_DIR . '/' . $siblingPluginBasename);
    }

    /**
     * Resolve the plugin basename currently being deleted from WordPress' uninstall action.
     *
     * @return string
     */
    private static function getDeletedPluginBasenameFromUninstallAction(): string
    {
        if (!function_exists('current_action')) {
            return '';
        }

        $action = current_action();
        if (!is_string($action) || strpos($action, self::UNINSTALL_ACTION_PREFIX) !== 0) {
            return '';
        }

        $pluginBasename = substr($action, strlen(self::UNINSTALL_ACTION_PREFIX));

        if (
            $pluginBasename !== self::FREE_PLUGIN_BASENAME
            && $pluginBasename !== self::PRO_PLUGIN_BASENAME
        ) {
            return '';
        }

        return $pluginBasename;
    }

    /**
     * Return the opposite Free/PRO artifact basename for a deleted artifact.
     *
     * @param string $deletedPluginBasename Deleted plugin basename.
     * @return string
     */
    private static function getSiblingPluginBasename(string $deletedPluginBasename): string
    {
        if ($deletedPluginBasename === self::FREE_PLUGIN_BASENAME) {
            return self::PRO_PLUGIN_BASENAME;
        }

        if ($deletedPluginBasename === self::PRO_PLUGIN_BASENAME) {
            return self::FREE_PLUGIN_BASENAME;
        }

        return '';
    }

    /**
     * Return whether an engine's required class set is complete.
     *
     * @param string   $label           Human-readable engine label.
     * @param string[] $requiredClasses Required class names.
     * @return bool
     */
    private static function canBootEngine(string $label, array $requiredClasses): bool
    {
        $missingClasses = self::missingRequiredClasses($requiredClasses);
        if ($missingClasses === []) {
            return true;
        }

        if (defined('WP_DEBUG') && WP_DEBUG) {
            // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Debug-only package integrity logging before engine loggers are guaranteed available.
            error_log(
                sprintf(
                    'Cache Autopilot bootstrap skipped %s engine: incomplete package, missing class %s.',
                    $label,
                    implode(', ', $missingClasses)
                )
            );
        }

        return false;
    }

    /**
     * Return required classes that are unavailable.
     *
     * @param string[] $classes Required class names.
     * @return string[]
     */
    private static function missingRequiredClasses(array $classes): array
    {
        $missingClasses = [];

        foreach ($classes as $class) {
            if (!class_exists($class, true)) {
                $missingClasses[] = $class;
            }
        }

        return $missingClasses;
    }

    /**
     * Run a callback only when its owning class is present, isolating failures
     * so one engine can never fatal the other.
     *
     * @param callable $callback     Work to execute.
     * @param string   $requiredClass Class that must exist before running.
     * @param string   $label        Human-readable label for logging.
     * @return void
     */
    private static function runGuarded(callable $callback, string $requiredClass, string $label): void
    {
        if (!class_exists($requiredClass, true)) {
            if (defined('WP_DEBUG') && WP_DEBUG) {
                // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Debug-only lifecycle guard logging before engine loggers are guaranteed available.
                error_log(sprintf('Cache Autopilot: %s skipped: missing class %s', $label, $requiredClass));
            }
            return;
        }

        try {
            $callback();
        } catch (\Throwable $error) {
            if (defined('WP_DEBUG') && WP_DEBUG) {
                // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Debug-only lifecycle guard logging before engine loggers are guaranteed available.
                error_log(sprintf('Cache Autopilot: %s error: %s', $label, $error->getMessage()));
            }
        }
    }
}
