<?php
declare(strict_types=1);

namespace Ekesto\CacheWarmup;

use Ekesto\CacheAutopilot\PluginMetadata;
use Ekesto\CacheWarmup\ImmediateKick;
use Ekesto\CacheWarmup\Settings\Repository;

/**
 * Plugin lifecycle orchestration.
 *
 * Handles activation/deactivation and version/schema upgrades.
 */
final class Lifecycle
{
    private const PLUGIN_VERSION_OPTION = 'ekesto_cache_warmup_plugin_version';
    private const DB_VERSION_OPTION = 'ekesto_cache_warmup_db_version';
    private const DB_VERSION = 4;
    private const UPGRADE_LOCK_OPTION = 'ekesto_cache_warmup_upgrade_lock';
    private const UPGRADE_LOCK_TTL = 30;

    /**
     * Register lifecycle hooks.
     *
     * @param string $pluginFile Main plugin file path.
     * @return void
     */
    public static function register(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);
    }

    /**
     * Handle plugin activation.
     *
     * @return void
     */
    public static function activate(): void
    {
        $schemaReady = self::createOrUpdateTables();
        self::persistVersionState($schemaReady);
    }

    /**
     * Handle plugin deactivation.
     *
     * @return void
     */
    public static function deactivate(): void
    {
        wp_clear_scheduled_hook('ekesto_cache_warmup_run');
        wp_clear_scheduled_hook('ekesto_cache_warmup_cleanup');
        wp_clear_scheduled_hook(Sitemap::MEMBERSHIP_REBUILD_HOOK);

        if (class_exists(ImmediateKick::class, true)) {
            ImmediateKick::clearPendingClaim();
        }
    }

    /**
     * Handle plugin uninstall.
     *
     * @return void
     */
    public static function uninstall(): void
    {
        self::clearScheduledHooks();
        self::deletePluginOptions();
        self::deletePluginTransients();
        Logger::dropTable();
    }

    /**
     * Run idempotent version/schema upgrade reconciliation.
     *
     * @return void
     */
    public static function maybeUpgrade(): void
    {
        if (self::isMaintenanceOrSensitiveUpdateRequest()) {
            return;
        }

        if (!self::acquireUpgradeLock()) {
            return;
        }

        try {
            $storedPluginVersion = (string) get_option(self::PLUGIN_VERSION_OPTION, '');
            $storedDbVersion = (int) get_option(self::DB_VERSION_OPTION, 0);
            self::maybeScheduleMembershipRebuildMigration();

            if ($storedPluginVersion === PluginMetadata::VERSION && $storedDbVersion >= self::DB_VERSION) {
                return;
            }

            self::migrateLegacyStorage();

            $schemaReady = $storedDbVersion >= self::DB_VERSION;
            if (!$schemaReady) {
                $schemaReady = self::createOrUpdateTables();
            }

            self::persistVersionState($schemaReady);
        } finally {
            self::releaseUpgradeLock();
        }
    }

    /**
     * Migrate durable legacy data and discard obsolete ephemeral state.
     *
     * @return void
     */
    private static function migrateLegacyStorage(): void
    {
        $missing = '__ekesto_ecw_option_missing__';
        $legacyTuningStats = get_option('ecw_tuning_stats', $missing);
        $newTuningStats = get_option('ekesto_ecw_tuning_stats', $missing);

        if ($newTuningStats === $missing && $legacyTuningStats !== $missing) {
            $migrated = add_option('ekesto_ecw_tuning_stats', $legacyTuningStats, '', false);
            if ($migrated || get_option('ekesto_ecw_tuning_stats', $missing) !== $missing) {
                delete_option('ecw_tuning_stats');
            }
        } elseif ($newTuningStats !== $missing) {
            delete_option('ecw_tuning_stats');
        }

        delete_transient('ecw_run_now_notice');
        delete_transient('ecw_upgrade_window_until');
        delete_transient('ecw_sitemap_rebuild_lock');
        delete_transient('ecw_sitemap_rebuild_scheduled_at');
    }

    /**
     * Persist lifecycle version state.
     *
     * @param bool $schemaReady Whether the schema is known to be up-to-date.
     * @return void
     */
    private static function persistVersionState(bool $schemaReady): void
    {
        update_option(self::PLUGIN_VERSION_OPTION, PluginMetadata::VERSION, false);

        if ($schemaReady) {
            update_option(self::DB_VERSION_OPTION, self::DB_VERSION, false);
            return;
        }

        Debug::warn('Lifecycle schema not ready; DB version flag not advanced.', [
            'db_version_target' => self::DB_VERSION,
        ]);
    }

    /**
     * Schedule membership rebuild migration when sitemap config exists but option is missing.
     *
     * @return void
     */
    private static function maybeScheduleMembershipRebuildMigration(): void
    {
        if (!is_admin() && !wp_doing_cron()) {
            return;
        }

        if (get_option(Settings::MEMBERSHIP_OPTION, false)) {
            return;
        }

        $sitemaps = Repository::getSitemaps();
        if ($sitemaps !== []) {
            Sitemap::scheduleMembershipRebuild(true);
        }
    }

    /**
     * Create or update plugin tables.
     *
     * @return bool True when table exists after dbDelta.
     */
    private static function createOrUpdateTables(): bool
    {
        global $wpdb;

        require_once ABSPATH . 'wp-admin/includes/upgrade.php';

        $charsetCollate = $wpdb->get_charset_collate();
        $table = Logger::getTableName();
        if ($table === '') {
            return false;
        }

        // Avoid reserved keywords (e.g., `trigger`) to keep compatibility with older MySQL/MariaDB.
        $sql = "CREATE TABLE {$table} (
            id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
            run_id CHAR(36) NOT NULL,
            site_id BIGINT(20) UNSIGNED NOT NULL DEFAULT 0,
            trigger_key VARCHAR(64) NOT NULL,
            mode VARCHAR(16) NOT NULL,
            status VARCHAR(16) NOT NULL,
            started_at DATETIME NOT NULL,
            updated_at DATETIME NULL,
            finished_at DATETIME NULL,
            total_urls INT(11) UNSIGNED NOT NULL DEFAULT 0,
            warmed_urls INT(11) UNSIGNED NOT NULL DEFAULT 0,
            failed_urls INT(11) UNSIGNED NOT NULL DEFAULT 0,
            message LONGTEXT NULL,
            details LONGTEXT NULL,
            PRIMARY KEY  (id),
            KEY run_id (run_id),
            KEY site_id (site_id),
            KEY trigger_key (trigger_key),
            KEY status (status),
            KEY started_at (started_at),
            KEY updated_at (updated_at)
        ) {$charsetCollate};";

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.SchemaChange -- Activation/upgrade schema reconciliation for the plugin-owned warmup log table.
        dbDelta($sql);

        // Verify table existence right after dbDelta().
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Activation/upgrade verification for the plugin-owned warmup log table.
        $exists = $wpdb->get_var(
            $wpdb->prepare('SHOW TABLES LIKE %s', $table)
        );

        $ok = (bool) $exists;

        Debug::info('Lifecycle table check', [
            'result' => $ok ? 'OK' : 'MISSING',
            'table' => $table,
            'last_error' => (string) $wpdb->last_error,
        ]);

        return $ok;
    }

    /**
     * Acquire a short-lived lock to avoid concurrent upgrade runs.
     *
     * @return bool
     */
    private static function acquireUpgradeLock(): bool
    {
        $now = time();
        $current = (int) get_option(self::UPGRADE_LOCK_OPTION, 0);

        if ($current > 0 && ($current + self::UPGRADE_LOCK_TTL) > $now) {
            return false;
        }

        update_option(self::UPGRADE_LOCK_OPTION, $now, false);
        return true;
    }

    /**
     * Release upgrade lock.
     *
     * @return void
     */
    private static function releaseUpgradeLock(): void
    {
        delete_option(self::UPGRADE_LOCK_OPTION);
    }

    /**
     * Clear plugin cron hooks.
     *
     * @return void
     */
    private static function clearScheduledHooks(): void
    {
        wp_clear_scheduled_hook('ekesto_cache_warmup_run');
        wp_clear_scheduled_hook('ekesto_cache_warmup_cleanup');
        wp_clear_scheduled_hook(Sitemap::MEMBERSHIP_REBUILD_HOOK);
    }

    /**
     * Delete plugin-owned options.
     *
     * @return void
     */
    private static function deletePluginOptions(): void
    {
        delete_option(Settings::OPTION);
        delete_option(Settings::SITEMAP_OPTION);
        delete_option(Settings::MEMBERSHIP_OPTION);
        delete_option(self::PLUGIN_VERSION_OPTION);
        delete_option(self::DB_VERSION_OPTION);
        delete_option(self::UPGRADE_LOCK_OPTION);
        delete_option(Sitemap::MEMBERSHIP_REBUILD_NEEDED_OPTION);
        delete_option(Sitemap::POST_TYPE_HASH_OPTION);
        delete_option('ekesto_cache_warmup_state');
        delete_option('ekesto_cache_warmup_preparing_buffer');
        delete_option('ekesto_cache_warmup_targeted_buffer');
        delete_option('ekesto_cache_warmup_targeted_mutation_lock');
        delete_option('ekesto_cache_warmup_run_lock');
        if (class_exists(ImmediateKick::class, true)) {
            ImmediateKick::clearPendingClaim();
        }
        TuningStats::clear();
    }

    /**
     * Delete plugin-owned transient state and transient-backed option rows.
     *
     * @return void
     */
    private static function deletePluginTransients(): void
    {
        delete_transient(Settings::SITEMAP_REFRESH_LOCK);
        delete_transient('ecw_run_now_notice');
        delete_transient('ecw_upgrade_window_until');
        delete_transient('ecw_sitemap_rebuild_lock');
        delete_transient('ecw_sitemap_rebuild_scheduled_at');
        delete_transient('ekesto_ecw_run_now_notice');
        delete_transient('ekesto_ecw_upgrade_window_until');
        delete_transient('ekesto_ecw_sitemap_rebuild_lock');
        delete_transient('ekesto_ecw_sitemap_rebuild_scheduled_at');
        delete_transient('ekesto_cache_warmup_priority_pages_v1');
        delete_transient('ekesto_cache_warmup_priority_pages_v2');
        delete_transient('ekesto_cache_warmup_priority_pages_v3');

        self::deleteOptionRowsByPatterns([
            '_transient_ekesto_ecw_pre_trash_permalink_',
            '_transient_timeout_ekesto_ecw_pre_trash_permalink_',
        ]);
    }

    /**
     * Delete option table rows matching the provided LIKE prefixes.
     *
     * @param string[] $prefixes Option-name prefixes before the SQL wildcard.
     * @return void
     */
    private static function deleteOptionRowsByPatterns(array $prefixes): void
    {
        global $wpdb;

        foreach ($prefixes as $prefix) {
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Uninstall cleanup must remove plugin-owned transient rows directly and must not use stale cache state.
            $wpdb->query(
                $wpdb->prepare(
                    'DELETE FROM %i WHERE option_name LIKE %s',
                    $wpdb->options,
                    $wpdb->esc_like($prefix) . '%'
                )
            );
        }
    }

    /**
     * Avoid upgrade writes during maintenance and sensitive installer/update requests.
     *
     * @return bool
     */
    private static function isMaintenanceOrSensitiveUpdateRequest(): bool
    {
        if (file_exists(ABSPATH . '.maintenance') || wp_is_maintenance_mode()) {
            return true;
        }

        if (defined('WP_INSTALLING') && WP_INSTALLING) {
            return true;
        }

        if (!is_admin()) {
            return false;
        }

        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only classification of installer/update requests to avoid upgrade writes.
        $page = isset($_REQUEST['page']) ? sanitize_key(wp_unslash((string) $_REQUEST['page'])) : '';
        if ($page === 'update-core') {
            return true;
        }

        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only classification of installer/update requests to avoid upgrade writes.
        $action = isset($_REQUEST['action']) ? sanitize_key(wp_unslash((string) $_REQUEST['action'])) : '';
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only classification of installer/update requests to avoid upgrade writes.
        $action2 = isset($_REQUEST['action2']) ? sanitize_key(wp_unslash((string) $_REQUEST['action2'])) : '';

        foreach (array_filter([$action, $action2]) as $candidate) {
            if (in_array($candidate, [
                'update',
                'update-plugin',
                'update-selected',
                'upgrade-plugin',
                'upload-plugin',
                'install-plugin',
                'activate-plugin',
            ], true)) {
                return true;
            }
        }

        return false;
    }
}
