<?php

namespace WPUltimateSecurity\UltimateSecurity\Bootstrap\Lifecycle;

use WPUltimateSecurity\UltimateSecurity\Infrastructure\Persistence\Schema\DatabaseSchema;
use WPUltimateSecurity\UltimateSecurity\Support\Helpers\Utils;
use WPUltimateSecurity\UltimateSecurity\Support\Helpers\DateHelper;
use WPUltimateSecurity\UltimateSecurity\Bootstrap\Lifecycle\Migrations\MigrationRunner;
use WPUltimateSecurity\UltimateSecurity\Bootstrap\Lifecycle\Migrations\MigrationRegistry;

/**
 * Database Updater
 *
 * Handles database schema updates and version-specific migrations.
 *
 * ============================================================================
 * DEVELOPER WORKFLOW FOR NEW RELEASES
 * ============================================================================
 *
 * For most releases (new tables, new options):
 * 1. Add table schema to DatabaseSchema.php (if needed)
 * 2. Add option defaults to Utils::ultimate_security_get_default_options() (if needed)
 * 3. Bump ULTIMATE_SECURITY_DB_VERSION in ultimate-security.php
 * 4. Done! The system handles the rest automatically.
 *
 * For bug fixes / UI-only changes:
 * - Only bump ULTIMATE_SECURITY_VERSION
 * - No DB version change needed
 *
 * For data migrations (rare):
 * - Create migration class in Migrations/ folder
 * - Register in MigrationRegistry::getMigrations()
 * - Bump ULTIMATE_SECURITY_DB_VERSION
 *
 * ============================================================================
 * UPDATE FLOW
 * ============================================================================
 *
 * 1. plugins_loaded (priority 0) - checkAndRunUpdates() runs BEFORE REST API
 * 2. Version comparison - Quick exit if versions match
 * 3. Lock acquisition - Prevents concurrent updates
 * 4. DatabaseSchema::create_tables() - Creates/updates tables via dbDelta
 * 5. DatabaseSchema::run_migrations() - Adds missing baseline columns
 * 6. MigrationRunner::run() - Runs version-specific data migrations
 * 7. mergeDefaultOptions() - Adds new options while preserving user settings
 * 8. Update version number
 *
 * ============================================================================
 * WHY plugins_loaded?
 * ============================================================================
 *
 * CRITICAL: This class hooks on `plugins_loaded` (priority 0) to ensure
 * updates run BEFORE the REST API registers (which happens on `init`).
 * This prevents race conditions where API calls access uninitialized data.
 *
 * Hook order: plugins_loaded → init (REST API) → admin_init
 *
 * Update Detection:
 * - Compares ULTIMATE_SECURITY_DB_VERSION constant with stored version
 * - Uses transient-based locking to prevent concurrent updates
 * - Runs on every request but exits early if version matches (minimal overhead)
 */
class DatabaseUpdater
{
    /**
     * Transient key for update lock
     */
    private const LOCK_KEY = 'ultimate_security_update_lock';

    /**
     * Lock timeout in seconds
     */
    private const LOCK_TIMEOUT = 30;

    /**
     * Option key for stored database schema version
     */
    private const DB_VERSION_OPTION = 'ultimate_security_db_version';

    /**
     * Option key for stored plugin version (catches upgrades without DB bumps)
     */
    private const PLUGIN_VERSION_OPTION = 'ultimate_security_plugin_version';

    /**
     * Flag to prevent multiple checks within same request
     */
    private static bool $hasCheckedVersion = false;

    /**
     * Register database updater hooks.
     *
     * IMPORTANT: Must be called early in the main plugin file, before init.
     *
     * @return void
     */
    public static function register(): void
    {
        // Run BEFORE init (REST API registers on init priority 0)
        // plugins_loaded fires after all plugins are loaded but before init
        add_action('plugins_loaded', [self::class, 'checkAndRunUpdates'], 0);

        // Also run on activation for fresh installs
        register_activation_hook(
            ULTIMATE_SECURITY_PATH . 'ultimate-security.php',
            [self::class, 'onActivation']
        );

        // Catch plugin updates via WordPress upgrader
        add_action('upgrader_process_complete', [self::class, 'onPluginUpdate'], 10, 2);
    }

    /**
     * Check version and run updates if needed.
     *
     * This runs on EVERY page load but uses quick version comparison
     * to exit early for most requests. Locking prevents race conditions.
     *
     * @return void
     */
    public static function checkAndRunUpdates(): void
    {
        // Only check once per request
        if (self::$hasCheckedVersion) {
            return;
        }
        self::$hasCheckedVersion = true;

        // Quick version check - most requests will exit here
        if (!self::needsUpdate()) {
            return;
        }

        // Acquire lock to prevent concurrent updates
        $lock = get_transient(self::LOCK_KEY);

        if ($lock) {
            // Another process is updating, skip
            return;
        }

        // Set lock with timeout
        set_transient(self::LOCK_KEY, time(), self::LOCK_TIMEOUT);

        try {
            self::runUpdates();
        } finally {
            // Always release lock
            delete_transient(self::LOCK_KEY);
        }
    }

    /**
     * Check if database needs update.
     *
     * @return bool True if update is needed
     */
    public static function needsUpdate(): bool
    {
        if (!defined('ULTIMATE_SECURITY_DB_VERSION')) {
            return false;
        }

        $currentDbVersion = get_option(self::DB_VERSION_OPTION, '0');
        $currentPluginVersion = get_option(self::PLUGIN_VERSION_OPTION, '0');

        $dbVersionOutdated = version_compare(ULTIMATE_SECURITY_DB_VERSION, $currentDbVersion, '>');
        $pluginVersionOutdated = defined('ULTIMATE_SECURITY_VERSION')
            ? version_compare(ULTIMATE_SECURITY_VERSION, $currentPluginVersion, '>')
            : false;

        // Also run if any expected tables are missing (handles missed version bumps)
        $tablesMissing = self::hasMissingTables();

        return $dbVersionOutdated || $pluginVersionOutdated || $tablesMissing;
    }

    /**
     * Run all necessary updates.
     *
     * @return void
     */
    private static function runUpdates(): void
    {
        $currentVersion = get_option(self::DB_VERSION_OPTION, '0');

        // Fresh install
        if ($currentVersion === '0') {
            self::freshInstall();
            return;
        }

        // Existing installation - run migrations
        self::updateInstallation();
    }

    /**
     * Handle fresh installation.
     *
     * For new installs, we create tables and set defaults.
     * All migrations are marked as complete since the database
     * starts at the latest schema.
     *
     * @return void
     */
    private static function freshInstall(): void
    {
        // Create all tables
        DatabaseSchema::create_tables();

        // Set default options
        $defaultOptions = Utils::ultimate_security_get_default_options();
        Utils::ultimate_security_update_option($defaultOptions);

        // Mark all migrations as complete (they're for upgrades only)
        MigrationRegistry::markAllCompleted();

        // Set version
        self::updateStoredVersions();
    }

    /**
     * Update existing installation.
     *
     * For upgrades, we:
     * 1. Create/update tables using dbDelta
     * 2. Run schema migrations for baseline columns
     * 3. Run version-specific migrations
     * 4. Merge new default options
     * 5. Clean up deprecated data
     *
     * @return void
     */
    private static function updateInstallation(): void
    {
        // Update table schema (dbDelta handles create/update)
        DatabaseSchema::create_tables();

        // Add missing baseline columns to existing tables
        DatabaseSchema::run_migrations();

        // Run version-specific migrations
        MigrationRunner::run();

        // Merge new default options with existing settings
        self::mergeDefaultOptions();

        // Migrate legacy data
        self::migrateLegacyData();

        // Drop deprecated tables
        self::dropDeprecatedTables();

        // Update version
        self::updateStoredVersions();
    }

    /**
     * Merge new default options with existing options.
     *
     * Uses array_replace_recursive to:
     * - Add new keys from defaults
     * - Preserve existing user settings
     *
     * IMPORTANT: Uses raw database values (not decrypted) to preserve encrypted API keys.
     *
     * @return void
     */
    private static function mergeDefaultOptions(): void
    {
        // Get RAW database values - do NOT use Utils::ultimate_security_get_option()
        // because it decrypts API keys, and we need to preserve them as-is.
        $currentOptions = get_option('ultimate_security_options', []);
        $defaultOptions = Utils::ultimate_security_get_default_options();

        // Deep merge: defaults first, then current (preserves user settings including encrypted API keys)
        $mergedOptions = array_replace_recursive($defaultOptions, $currentOptions);

        Utils::ultimate_security_update_option($mergedOptions);
    }

    /**
     * Migrate legacy data structures.
     *
     * Handles one-time data migrations from old storage formats.
     *
     * @return void
     */
    private static function migrateLegacyData(): void
    {
        self::migrateRequestslogData();
    }

    /**
     * Migrate URL Guard data from wp_options to custom database table.
     *
     * @return void
     */
    private static function migrateRequestslogData(): void
    {
        global $wpdb;

        // Check if migration has already been run
        if (get_option('ultimate_security_url_guard_requestslog_data_migrated')) {
            return;
        }

        $table = $wpdb->prefix . 'ultimate_security_url_guard_requestslog';

        // Check if table exists
        $tableExists = $wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $table));
        if (!$tableExists) {
            return;
        }

        // Migrate from wp_options
        $ultimateSecurityOptions = Utils::ultimate_security_get_option();
        $requestsLog = $ultimateSecurityOptions['security']['url_guard']['requests_log'] ?? [];

        if (!empty($requestsLog)) {
            foreach ($requestsLog as $value) {
                $wpdb->insert(
                    $table,
                    [
                        'request_uri' => $value['request_uri'] ?? '',
                        'query_param' => $value['key'] ?? '',
                        'status' => $value['blocked'] ?? 'allow',
                        'created_at' => gmdate('Y-m-d H:i:s', $value['time'] ?? time()),
                    ],
                    ['%s', '%s', '%s', '%s']
                );
            }
        }

        // Mark migration as complete
        update_option('ultimate_security_url_guard_requestslog_data_migrated', DateHelper::now());
    }

    /**
     * Drop deprecated tables.
     *
     * @return void
     */
    private static function dropDeprecatedTables(): void
    {
        global $wpdb;
        $wpdb->query("DROP TABLE IF EXISTS {$wpdb->prefix}ultimate_security_histories");
        $wpdb->query("DROP TABLE IF EXISTS {$wpdb->prefix}ultimate_security_historymeta");
    }

    /**
     * Handle plugin activation.
     *
     * @return void
     */
    public static function onActivation(): void
    {
        // Reset the check flag to ensure updates run
        self::$hasCheckedVersion = false;
        self::checkAndRunUpdates();
    }

    /**
     * Handle plugin update via WordPress upgrader.
     *
     * @param object $upgraderObject WP_Upgrader instance
     * @param array $options Update options
     * @return void
     */
    public static function onPluginUpdate($upgraderObject, $options): void
    {
        if (!isset($options['action']) || $options['action'] !== 'update') {
            return;
        }

        if (!isset($options['type']) || $options['type'] !== 'plugin') {
            return;
        }

        $currentPlugin = plugin_basename(ULTIMATE_SECURITY_PATH . 'ultimate-security.php');
        $plugins = $options['plugins'] ?? [];

        if (in_array($currentPlugin, $plugins, true)) {
            // Reset the check flag and run updates
            self::$hasCheckedVersion = false;
            self::checkAndRunUpdates();
        }
    }

    /**
     * Get current database version.
     *
     * @return string Current version or '0' if not set
     */
    public static function getCurrentVersion(): string
    {
        return get_option(self::DB_VERSION_OPTION, '0');
    }

    /**
     * Check if any defined plugin tables are missing.
     *
     * @return bool True when at least one table is missing
     */
    private static function hasMissingTables(): bool
    {
        global $wpdb;

        foreach (DatabaseSchema::get_all_full_table_names() as $table) {
            $tableExists = $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $table));

            if (!$tableExists) {
                return true;
            }
        }

        return false;
    }

    /**
     * Persist the plugin and database schema versions after successful updates.
     *
     * @return void
     */
    private static function updateStoredVersions(): void
    {
        if (defined('ULTIMATE_SECURITY_DB_VERSION')) {
            update_option(self::DB_VERSION_OPTION, ULTIMATE_SECURITY_DB_VERSION);
        }

        if (defined('ULTIMATE_SECURITY_VERSION')) {
            update_option(self::PLUGIN_VERSION_OPTION, ULTIMATE_SECURITY_VERSION);
        }
    }

    /**
     * Force release update lock.
     *
     * Use with caution - only for debugging stuck updates.
     *
     * @return void
     */
    public static function forceReleaseLock(): void
    {
        delete_transient(self::LOCK_KEY);
    }
}
