<?php

declare(strict_types=1);

namespace SecurWP\Modules\Cloud;

if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

use SecurWP\Includes\Interfaces\Module;
use SecurWP\Core\Database\SchemaInspector;
use SecurWP\Modules\Cloud\License\LicenseManager;
use SecurWP\Modules\Cloud\Service\CloudSettings;
use SecurWP\Modules\Cloud\Services\StatePush;
use SecurWP\Modules\Cloud\Services\SiteMigrationService;

/**
 * The Cloud module class.
 * Coordinates connection to AWS Lambda Cloud Services.
 */
class Cloud implements Module {
    private const VULNERABILITY_REFRESH_HOOK = 'securwp_vulnerability_refresh';
    private const VULNERABILITY_REFRESH_GROUP = 'securwp_cloud';
    private const VULNERABILITY_REFRESH_DELAY = 15;
    private const VULNERABILITY_REFRESH_RETRY_DELAY = 60;
    private const VULNERABILITY_REFRESH_PENDING_OPTION = 'securwp_vulnerability_refresh_pending';
    private const RETIRED_FIREWALL_RULE_SYNC_CLEANUP_OPTION = 'securwp_retired_firewall_rule_sync_cleaned_v1';

    private $plugin_name;
    private $version;
    private $license_manager;
    private $settings;

    public function __construct( string $plugin_name, string $version ) {
        $this->plugin_name = $plugin_name;
        $this->version = $version;
        $this->load_settings();
    }

    private function load_settings() {
        $defaults = [
            'license_key' => '',
            'license_status' => '',
            'license_plan' => 'free',
            'site_token' => '',
            'site_secret' => '',
            'last_sync_vuln' => 0,
            'last_sync_malware' => 0,
        ];
        $this->settings = CloudSettings::get_all();
        $this->settings = array_merge( $defaults, $this->settings );
    }

    public function initialize( \SecurWP\Includes\Module_Loader $loader ): void {
        $this->cleanup_retired_firewall_rule_sync();
        $this->license_manager = new LicenseManager( $this->settings );
        (new StatePush())->register_hooks();
        $this->ensure_hourly_metrics_cron();

        add_filter( 'cron_schedules', [ $this, 'register_cron_schedules' ] );

        $loader->add_action( 'securwp_daily_event', $this, 'run_daily_syncs' );
        $loader->add_action( 'upgrader_process_complete', $this, 'sync_vulns_on_upgrade', 10, 2 );
        $loader->add_action( self::VULNERABILITY_REFRESH_HOOK, $this, 'handle_scheduled_vulnerability_refresh', 10, 0 );
        $loader->add_action( 'securwp_scan_completed', $this, 'handle_scan_terminal_for_pending_vulnerability_refresh', 10, 0 );
        $loader->add_action( 'securwp_scan_failed', $this, 'handle_scan_terminal_for_pending_vulnerability_refresh', 10, 0 );
        $loader->add_action( 'securwp_scan_stopped', $this, 'handle_scan_terminal_for_pending_vulnerability_refresh', 10, 0 );
        $loader->add_action( 'securwp_scan_cancelled', $this, 'handle_scan_terminal_for_pending_vulnerability_refresh', 10, 0 );

        $this->ensure_scheduled_events();

        $loader->add_action( 'rest_api_init', $this, 'register_rest_routes', 10, 0 );
        $loader->add_action( 'wp_ajax_securwp_request_otp', $this, 'handle_ajax_request_otp' );
        $loader->add_action( 'wp_ajax_securwp_request_license_otp', $this, 'handle_ajax_request_license_otp' );
        $loader->add_action( 'wp_ajax_securwp_verify_otp', $this, 'handle_ajax_verify_otp' );
        $loader->add_action( 'wp_ajax_securwp_console_connect_init', $this, 'handle_ajax_console_connect_init' );
        $loader->add_action( 'wp_ajax_securwp_console_connect_preview', $this, 'handle_ajax_console_connect_preview' );
        $loader->add_action( 'wp_ajax_securwp_console_connect_exchange', $this, 'handle_ajax_console_connect_exchange' );
        $loader->add_action( 'wp_ajax_securwp_retry_pro_activation', $this, 'handle_ajax_retry_pro_activation' );
        $loader->add_action( 'wp_ajax_securwp_activate_free', $this, 'handle_ajax_activate_free' );
        $loader->add_action( 'wp_ajax_securwp_activate_pro', $this, 'handle_ajax_activate_pro' );
        $loader->add_action( 'wp_ajax_securwp_deactivate_license', $this, 'handle_ajax_deactivate' );
        $loader->add_action( 'wp_ajax_securwp_revalidate_license', $this, 'handle_ajax_revalidate' );
        $loader->add_action( 'wp_ajax_securwp_check_license', $this, 'handle_ajax_check' );
        $loader->add_action( 'wp_ajax_securwp_verify_license_key', $this, 'handle_ajax_verify_license_key' );
        $loader->add_action( 'admin_init', $this, 'maybe_auto_activate_from_constant' );
        $loader->add_action( 'admin_init', $this, 'register_privacy_policy_content' );
        $loader->add_action( 'admin_notices', null, [ SiteMigrationService::class, 'render_admin_notice' ] );
    }

    /**
     * Register privacy policy disclosure text for the WordPress Privacy Policy guide.
     */
    public function register_privacy_policy_content(): void {
        if ( ! function_exists( 'wp_add_privacy_policy_content' ) ) {
            return;
        }

        $content = wp_kses_post( sprintf(
            /* translators: Privacy policy content for the SecurWP plugin. */
            '<h2>%1$s</h2>
            <p>%2$s</p>
            <ul>
                <li><strong>SecurWP Cloud (intel.securewp.net, console.securewp.net)</strong> &mdash; %3$s <a href="https://securewp.net/privacy-policy/" target="_blank">%4$s</a></li>
                <li><strong>MaxMind GeoLite2</strong> &mdash; %5$s <a href="https://www.maxmind.com/en/privacy-policy" target="_blank">%4$s</a></li>
                <li><strong>Have I Been Pwned (haveibeenpwned.com)</strong> &mdash; %6$s <a href="https://haveibeenpwned.com/Privacy" target="_blank">%4$s</a></li>
                <li><strong>Google reCAPTCHA</strong> &mdash; %7$s <a href="https://policies.google.com/privacy" target="_blank">%4$s</a></li>
                <li><strong>Cloudflare Turnstile</strong> &mdash; %8$s <a href="https://www.cloudflare.com/privacypolicy/" target="_blank">%4$s</a></li>
                <li><strong>Cloudflare API</strong> &mdash; %9$s <a href="https://www.cloudflare.com/privacypolicy/" target="_blank">%4$s</a></li>
            </ul>',
            __( 'SecurWP', 'securwp' ),
            __( 'SecurWP may contact the following external services depending on which features you enable:', 'securwp' ),
            __( 'When connected, sends site metadata, plugin/theme version lists, firewall metrics, and unverified file contents for malware analysis. No data is sent unless the site is connected to a SecurWP account (site token and secret are present).', 'securwp' ),
            __( 'Privacy Policy', 'securwp' ),
            __( 'Used for GeoIP country blocking when a MaxMind license key is configured. Downloads a database file; no visitor data is sent to MaxMind.', 'securwp' ),
            __( 'When breached password detection is enabled, the first 5 characters of a SHA-1 hash of the password are sent to check against known breached passwords. The full password is never transmitted.', 'securwp' ),
            __( 'When Google reCAPTCHA is enabled for the login form, visitor IP addresses and browser data are sent to Google for CAPTCHA verification.', 'securwp' ),
            __( 'When Cloudflare Turnstile is enabled for the login form, visitor data is sent to Cloudflare for CAPTCHA verification.', 'securwp' ),
            __( 'When the Cloudflare integration is enabled, blocked IP addresses are synced to your Cloudflare account via the Cloudflare API.', 'securwp' )
        ) );

        wp_add_privacy_policy_content( 'SecurWP', $content );
    }

    public function register_rest_routes(): void {
        $ns   = 'securwp/v1';
        $perm = [ $this, 'check_rest_permission' ];

        register_rest_route( $ns, '/license/otp/request', [
            [ 'methods' => 'POST', 'callback' => [ $this, 'rest_request_otp' ], 'permission_callback' => $perm ],
        ] );
        register_rest_route( $ns, '/license/otp/request-by-key', [
            [ 'methods' => 'POST', 'callback' => [ $this, 'rest_request_license_otp' ], 'permission_callback' => $perm ],
        ] );
        register_rest_route( $ns, '/license/otp/verify', [
            [ 'methods' => 'POST', 'callback' => [ $this, 'rest_verify_otp' ], 'permission_callback' => $perm ],
        ] );
        register_rest_route( $ns, '/license/activate/free', [
            [ 'methods' => 'POST', 'callback' => [ $this, 'rest_activate_free' ], 'permission_callback' => $perm ],
        ] );
        register_rest_route( $ns, '/license/activate/pro', [
            [ 'methods' => 'POST', 'callback' => [ $this, 'rest_activate_pro' ], 'permission_callback' => $perm ],
        ] );
        register_rest_route( $ns, '/license/activate/retry', [
            [ 'methods' => 'POST', 'callback' => [ $this, 'rest_retry_pro' ], 'permission_callback' => $perm ],
        ] );
        register_rest_route( $ns, '/license/deactivate', [
            [ 'methods' => 'POST', 'callback' => [ $this, 'rest_deactivate' ], 'permission_callback' => $perm ],
        ] );
        register_rest_route( $ns, '/license/migration/replace', [
            [ 'methods' => 'POST', 'callback' => [ $this, 'rest_migration_replace' ], 'permission_callback' => $perm ],
        ] );
        register_rest_route( $ns, '/license/migration/connect-new', [
            [ 'methods' => 'POST', 'callback' => [ $this, 'rest_migration_connect_new' ], 'permission_callback' => $perm ],
        ] );
        register_rest_route( $ns, '/license/console/init', [
            [ 'methods' => 'POST', 'callback' => [ $this, 'rest_console_init' ], 'permission_callback' => $perm ],
        ] );
        register_rest_route( $ns, '/license/console/exchange', [
            [ 'methods' => 'POST', 'callback' => [ $this, 'rest_console_exchange' ], 'permission_callback' => $perm ],
        ] );
        register_rest_route( $ns, '/key/restore-otp', [
            [ 'methods' => 'POST', 'callback' => [ $this, 'rest_key_restore_otp' ], 'permission_callback' => $perm ],
        ] );
        register_rest_route( $ns, '/key/restore', [
            [ 'methods' => 'POST', 'callback' => [ $this, 'rest_key_restore' ], 'permission_callback' => $perm ],
        ] );
        register_rest_route( $ns, '/key/generate-new', [
            [ 'methods' => 'POST', 'callback' => [ $this, 'rest_key_generate_new' ], 'permission_callback' => $perm ],
        ] );
        register_rest_route( $ns, '/license/verify-key', [
            [ 'methods' => 'POST', 'callback' => [ $this, 'rest_verify_license_key' ], 'permission_callback' => $perm ],
        ] );
    }

    public function check_rest_permission(): bool {
        return \SecurWP\Includes\Capabilities::can_manage_plugin();
    }

    public function rest_request_otp( \WP_REST_Request $request ): \WP_REST_Response {
        $email = sanitize_email( $request->get_param( 'email' ) ?? '' );
        if ( empty( $email ) ) {
            return new \WP_REST_Response( [ 'message' => 'Email is required.' ], 400 );
        }
        return $this->rest_respond( $this->license_manager->request_otp( $email ) );
    }

    public function rest_request_license_otp( \WP_REST_Request $request ): \WP_REST_Response {
        $key = sanitize_text_field( $request->get_param( 'license_key' ) ?? '' );
        if ( empty( $key ) ) {
            return new \WP_REST_Response( [ 'message' => 'License key is required.' ], 400 );
        }
        return $this->rest_respond( $this->license_manager->request_license_otp( $key ) );
    }

    public function rest_verify_otp( \WP_REST_Request $request ): \WP_REST_Response {
        $email = sanitize_email( $request->get_param( 'email' ) ?? '' );
        $otp   = sanitize_text_field( $request->get_param( 'otp' ) ?? '' );
        if ( empty( $email ) || empty( $otp ) ) {
            return new \WP_REST_Response( [ 'message' => 'Email and OTP are required.' ], 400 );
        }
        return $this->rest_respond( $this->license_manager->verify_otp( $email, $otp ) );
    }

    public function rest_verify_license_key( \WP_REST_Request $request ): \WP_REST_Response {
        $key = sanitize_text_field( $request->get_param( 'license_key' ) ?? '' );
        $otp = sanitize_text_field( $request->get_param( 'otp' ) ?? '' );
        $email = sanitize_email( $request->get_param( 'email' ) ?? '' );
        if ( empty( $key ) ) {
            return new \WP_REST_Response( [ 'message' => 'License key is required.' ], 400 );
        }
        if ( empty( $otp ) ) {
            return new \WP_REST_Response( [ 'message' => 'OTP is required.' ], 400 );
        }
        return $this->rest_respond( $this->license_manager->verify_license( $key, $email, $otp ) );
    }

    public function rest_activate_free( \WP_REST_Request $request ): \WP_REST_Response {
        $token = sanitize_text_field( $request->get_param( 'activation_session_token' ) ?? '' );
        if ( empty( $token ) ) {
            return new \WP_REST_Response( [ 'message' => 'Activation session token is required.' ], 400 );
        }
        return $this->rest_respond( $this->license_manager->activate_free( $token ) );
    }

    public function rest_activate_pro( \WP_REST_Request $request ): \WP_REST_Response {
        $err = $this->credential_guard();
        if ( $err ) { return $err; }
        $before = CloudSettings::get_all();
        $result = $this->license_manager->activate_pro();
        if ( is_wp_error( $result ) ) {
            return $this->rest_respond( $result );
        }
        $this->load_settings();
        return new \WP_REST_Response( [
            'message'         => (string) ( $result['message'] ?? 'Pro activation completed.' ),
            'status'          => (string) ( $this->settings['license_status'] ?? '' ),
            'plan'            => (string) ( $this->settings['license_plan'] ?? 'free' ),
            'previous_status' => (string) ( $before['license_status'] ?? '' ),
            'previous_plan'   => (string) ( $before['license_plan'] ?? 'free' ),
        ], 200 );
    }

    public function rest_retry_pro( \WP_REST_Request $request ): \WP_REST_Response {
        $token = sanitize_text_field( $request->get_param( 'activation_session_token' ) ?? '' );
        if ( empty( $token ) ) {
            return new \WP_REST_Response( [ 'message' => 'Activation session token is required.' ], 400 );
        }
        return $this->rest_respond( $this->license_manager->retry_pro_activation( $token ) );
    }

    public function rest_deactivate( \WP_REST_Request $request ): \WP_REST_Response {
        $this->license_manager->deactivate();
        return new \WP_REST_Response( [ 'message' => 'License deactivated.', 'status' => 'inactive', 'plan' => 'free' ], 200 );
    }

    public function rest_migration_replace( \WP_REST_Request $request ): \WP_REST_Response {
        $err = $this->credential_guard();
        if ( $err ) { return $err; }

        $result = $this->license_manager->resolve_migration_replace();
        if ( is_wp_error( $result ) ) {
            return $this->rest_respond( $result );
        }

        $this->load_settings();

        return new \WP_REST_Response( [
            'message'          => 'Site replacement completed.',
            'status'           => (string) ( $this->settings['license_status'] ?? '' ),
            'plan'             => (string) ( $this->settings['license_plan'] ?? 'free' ),
            'migration_status' => (string) ( $this->settings['migration_status'] ?? 'none' ),
        ], 200 );
    }

    public function rest_migration_connect_new( \WP_REST_Request $request ): \WP_REST_Response {
        $err = $this->credential_guard();
        if ( $err ) { return $err; }

        $result = $this->license_manager->resolve_migration_connect_new();
        if ( is_wp_error( $result ) ) {
            return $this->rest_respond( $result );
        }

        $this->load_settings();

        return new \WP_REST_Response( [
            'message'          => 'This copy is ready to be connected as a separate site.',
            'status'           => (string) ( $this->settings['license_status'] ?? '' ),
            'plan'             => (string) ( $this->settings['license_plan'] ?? 'free' ),
            'migration_status' => (string) ( $this->settings['migration_status'] ?? 'approved_connect_new' ),
        ], 200 );
    }

    public function rest_console_init( \WP_REST_Request $request ): \WP_REST_Response {
        $return_to = admin_url( 'admin.php?page=securwp-settings&tab=license' );
        return $this->rest_respond( $this->license_manager->console_connect_init( $return_to ) );
    }

    public function rest_console_exchange( \WP_REST_Request $request ): \WP_REST_Response {
        $code = sanitize_text_field( $request->get_param( 'code' ) ?? '' );
        if ( empty( $code ) ) {
            return new \WP_REST_Response( [ 'message' => 'Missing exchange code.' ], 400 );
        }
        return $this->rest_respond( $this->license_manager->console_connect_exchange( $code ) );
    }

    /**
     * Request OTP for key backup retrieval from Console.
     */
    public function rest_key_restore_otp( \WP_REST_Request $request ): \WP_REST_Response {
        $result = Service\KeyBackupService::request_restore_otp();
        if ( is_wp_error( $result ) ) {
            return new \WP_REST_Response( [
                'code'    => $result->get_error_code(),
                'message' => $result->get_error_message(),
            ], 400 );
        }

        return new \WP_REST_Response( $result, 200 );
    }

    /**
     * Retrieve key backup from Console and restore locally.
     */
    public function rest_key_restore( \WP_REST_Request $request ): \WP_REST_Response {
        $email = sanitize_email( $request->get_param( 'email' ) ?? '' );
        $otp   = sanitize_text_field( $request->get_param( 'otp' ) ?? '' );

        if ( $email === '' || $otp === '' ) {
            return new \WP_REST_Response( [
                'code'    => 'missing_params',
                'message' => 'Email and OTP are required.',
            ], 400 );
        }

        $result = Service\KeyBackupService::retrieve_and_restore( $email, $otp );
        if ( is_wp_error( $result ) ) {
            return new \WP_REST_Response( [
                'code'    => $result->get_error_code(),
                'message' => $result->get_error_message(),
            ], 400 );
        }

        return new \WP_REST_Response( [ 'message' => 'Encryption key restored successfully.' ], 200 );
    }

    /**
     * Generate a brand new encryption key and clear all encrypted data.
     * Last resort when no backup is available.
     */
    public function rest_key_generate_new( \WP_REST_Request $request ): \WP_REST_Response {
        // Delete existing key file.
        $key_path = \SecurWP\Includes\MasterKey::get_key_file_path();
        if ( file_exists( $key_path ) ) {
            if ( ! wp_delete_file( $key_path ) ) {
                return new \WP_REST_Response( [
                    'code'    => 'delete_failed',
                    'message' => 'Could not remove the existing key file. Check file permissions.',
                ], 500 );
            }
        }

        // Generate fresh key.
        \SecurWP\Includes\MasterKey::clear_cache();
        try {
            \SecurWP\Includes\MasterKey::get();
        } catch ( \RuntimeException $e ) {
            securwp_log( '[SecurWP] Key generation failed: ' . $e->getMessage() );
            return new \WP_REST_Response( [
                'code'    => 'generate_failed',
                'message' => 'Could not create a new key file. Check directory permissions and PHP error logs for details.',
            ], 500 );
        }

        // Clear all stale encrypted data (undecryptable with new key).
        CloudSettings::update_stored_fields( [
            'site_token_enc'  => '',
            'site_secret_enc' => '',
            'license_status'  => '',
            'license_plan'    => '',
        ] );

        // Clear encrypted wp_options through the Options API so persistent
        // object caches are invalidated for every removed secret.
        \SecurWP\Includes\SecretManager::delete_all();

        global $wpdb;

        // Reset key-bound TOTP secrets and clear 2FA recovery/session state.
        $table = $wpdb->prefix . 'securwp_two_factor';
        $recovery_table = $wpdb->prefix . 'securwp_two_factor_recovery_codes';
        $session_table  = $wpdb->prefix . 'securwp_two_factor_sessions';
        $now            = time();
        if ( SchemaInspector::table_exists( $table ) ) {
            $table_identifier = SchemaInspector::table_identifier( $table );
            // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,PluginCheck.Security.DirectDB.UnescapedDBParameter,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
            $wpdb->query( $wpdb->prepare(
                "UPDATE {$table_identifier}
                 SET totp_secret = NULL,
                     method = %s,
                     is_enabled = 0,
                     confirmed_at = NULL,
                     remember_token = NULL,
                     remember_expires = NULL,
                     updated_at = %d",
                'totp',
                $now
            ) );
            // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,PluginCheck.Security.DirectDB.UnescapedDBParameter,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
        }

        if ( SchemaInspector::table_exists( $recovery_table ) ) {
            \SecurWP\Modules\TwoFactor\RecoveryCodes::delete_all();
        }

        if ( SchemaInspector::table_exists( $session_table ) ) {
            \SecurWP\Modules\TwoFactor\LoginSessionStore::delete_all();
        }

        \SecurWP\Includes\SettingsStore::delete_field( \SecurWP\Includes\SettingsStore::COL_STATE, 'key_backup_available' );

        return new \WP_REST_Response( [
            'message' => 'New encryption key generated. Encrypted data, authenticator app secrets, recovery codes, and pending two-factor sessions have been cleared.',
        ], 200 );
    }

    /**
     * Return a 409 response if credentials are broken or missing (for REST handlers).
     */
    private function credential_guard(): ?\WP_REST_Response {
        $error = $this->settings['_key_error'] ?? null;
        if ( $error ) {
            return new \WP_REST_Response( [
                'code'    => 'key_error',
                'message' => 'Encryption key issue detected. Resolve the key error above before continuing.',
            ], 409 );
        }

        // Also block when credentials are simply absent (site not connected).
        $token  = (string) ( $this->settings['site_token'] ?? '' );
        $secret = (string) ( $this->settings['site_secret'] ?? '' );
        if ( $token === '' || $secret === '' ) {
            return new \WP_REST_Response( [
                'code'    => 'not_connected',
                'message' => 'This site is not connected to SecurWP Console. Go to Settings to connect.',
            ], 409 );
        }

        return null;
    }

    /**
     * Send a JSON error and die if credentials are broken or missing (for AJAX handlers).
     *
     * @return bool True if guard fired (caller should return), false if OK.
     */
    private function credential_guard_ajax(): bool {
        $error = $this->settings['_key_error'] ?? null;
        if ( $error ) {
            wp_send_json_error( [
                'code'    => 'key_error',
                'message' => 'Encryption key issue detected. Resolve the key error above before continuing.',
            ] );
            return true; // wp_send_json_error calls die(), but for static analysis.
        }

        // Also block when credentials are simply absent (site not connected).
        $token  = (string) ( $this->settings['site_token'] ?? '' );
        $secret = (string) ( $this->settings['site_secret'] ?? '' );
        if ( $token === '' || $secret === '' ) {
            wp_send_json_error( [
                'code'    => 'not_connected',
                'message' => 'This site is not connected to SecurWP Console. Go to Settings to connect.',
            ] );
            return true;
        }

        return false;
    }

    /**
     * Convert LicenseManager result to REST response.
     */
    private function rest_respond( $result ): \WP_REST_Response {
        if ( is_wp_error( $result ) ) {
            $data   = $result->get_error_data();
            $status = is_array( $data ) ? (int) ( $data['status'] ?? 400 ) : 400;
            $payload = [ 'message' => $result->get_error_message() ];
            if ( is_array( $data ) && isset( $data['detail'] ) ) {
                $payload['detail'] = $data['detail'];
            }
            return new \WP_REST_Response( $payload, $status );
        }
        return new \WP_REST_Response( is_array( $result ) ? $result : [ 'success' => true ], 200 );
    }

    /**
     * WP-Cron schedules (used only when Action Scheduler is not present).
     *
     * @param array $schedules
     * @return array
     */
    public function register_cron_schedules( $schedules ) {
        if ( ! isset( $schedules['securwp_five_minutes'] ) ) {
            $schedules['securwp_five_minutes'] = [
                'interval' => 300,
                // Keep pre-init safe: cron_schedules can be evaluated before init.
                'display'  => 'Every 5 Minutes (SecurWP)',
            ];
        }
        return $schedules;
    }

    /**
     * Schedule daily event and clean up removed periodic jobs (idempotent).
     *
     * State push is now event-driven (fires on settings/plugin/theme changes)
     * so only the daily sync needs a recurring schedule.
     */
    private function ensure_scheduled_events(): void {
        // Clean up obsolete periodic jobs.
        $this->cleanup_removed_schedules();

        if ( function_exists( '\\as_has_scheduled_action' ) && function_exists( '\\as_schedule_recurring_action' ) ) {
            if ( ! \as_has_scheduled_action( 'securwp_daily_event' ) ) {
                \as_schedule_recurring_action( time() + 120, DAY_IN_SECONDS, 'securwp_daily_event', [], 'securwp_cloud' );
            }
            return;
        }

        // WP-Cron fallback.
        if ( ! wp_next_scheduled( 'securwp_daily_event' ) ) {
            wp_schedule_event( time() + 120, 'daily', 'securwp_daily_event' );
        }
    }

    /**
     * Remove obsolete periodic schedules from previous releases.
     */
    private function cleanup_removed_schedules(): void {
        $removed_hooks = [
            'securwp_telemetry_heartbeat',
            'securwp_state_push',
            'securwp_keepalive',
        ];

        foreach ( $removed_hooks as $hook ) {
            // Action Scheduler.
            if ( function_exists( '\\as_unschedule_all_actions' ) ) {
                \as_unschedule_all_actions( $hook );
            }
            // WP-Cron.
            $ts = wp_next_scheduled( $hook );
            if ( $ts ) {
                wp_unschedule_event( $ts, $hook );
            }
        }
    }

    /**
     * Register the hourly metrics push cron event.
     */
    private function ensure_hourly_metrics_cron(): void {
        if ( ! $this->should_schedule_metrics_push() ) {
            if ( $this->has_hourly_metrics_cron() ) {
                \SecurWP\Core\Installer::clear_scheduled_hook_everywhere( 'securwp_metrics_push' );
            }
            return;
        }

        if ( function_exists( '\\as_has_scheduled_action' ) && function_exists( '\\as_schedule_recurring_action' ) ) {
            if ( ! \as_has_scheduled_action( 'securwp_metrics_push' ) ) {
                \as_schedule_recurring_action( time() + 300, HOUR_IN_SECONDS, 'securwp_metrics_push', [], 'securwp_cloud' );
            }
            return;
        }

        // WP-Cron fallback.
        if ( ! wp_next_scheduled( 'securwp_metrics_push' ) ) {
            wp_schedule_event( time() + 300, 'hourly', 'securwp_metrics_push' );
        }
    }

    private function should_schedule_metrics_push(): bool {
        $migration_status = strtolower( trim( (string) ( $this->settings['migration_status'] ?? 'none' ) ) );

        return ! empty( $this->settings['console_enabled'] )
            && ! empty( $this->settings['site_token'] )
            && ! empty( $this->settings['site_secret'] )
            && ( $migration_status === '' || $migration_status === 'none' );
    }

    private function has_hourly_metrics_cron(): bool {
        if ( wp_next_scheduled( 'securwp_metrics_push' ) ) {
            return true;
        }

        if ( function_exists( '\\as_has_scheduled_action' ) ) {
            return (bool) \as_has_scheduled_action( 'securwp_metrics_push', [], 'securwp_cloud' )
                || (bool) \as_has_scheduled_action( 'securwp_metrics_push' );
        }

        return false;
    }

    public function run_daily_syncs() {
        $this->license_manager->check_status();

        if ( $this->allows_scheduled_vulnerability_refresh() ) {
            $this->queue_vulnerability_refresh( 'daily_sync' );
        }
        (new StatePush())->run();
    }

    /**
     * Remove stored state and local artifacts from the retired cloud firewall
     * rules sync feature.
     */
    private function cleanup_retired_firewall_rule_sync(): void {
        if ( get_option( self::RETIRED_FIREWALL_RULE_SYNC_CLEANUP_OPTION ) ) {
            return;
        }

        CloudSettings::cleanup_retired_fields();

        if ( function_exists( 'wp_cache_delete' ) ) {
            wp_cache_delete( 'securwp_cloud_rules', 'securwp' );
        }

        delete_transient( 'securwp_fw_rules_sync_probe' );

        $rules_dir = WP_CONTENT_DIR . '/securwp-data/firewall';
        $this->delete_directory_recursive( $rules_dir );

        update_option( self::RETIRED_FIREWALL_RULE_SYNC_CLEANUP_OPTION, '1', false );
    }

    private function delete_directory_recursive( string $path ): void {
        if ( ! is_dir( $path ) ) {
            return;
        }

        global $wp_filesystem;

        if ( ! ( $wp_filesystem instanceof \WP_Filesystem_Base ) ) {
            require_once ABSPATH . 'wp-admin/includes/file.php';
            WP_Filesystem();
        }

        if ( $wp_filesystem instanceof \WP_Filesystem_Base ) {
            $wp_filesystem->delete( $path, true );
        }
    }

    /**
     * Re-sync vulnerability data when plugins or themes are updated.
     *
     * @param \WP_Upgrader $upgrader
     * @param array        $hook_extra
     */
    public function sync_vulns_on_upgrade( $upgrader, $hook_extra ): void {
        $type   = $hook_extra['type'] ?? '';
        $action = $hook_extra['action'] ?? '';

        if ( $action !== 'update' || ! in_array( $type, [ 'plugin', 'theme' ], true ) ) {
            return;
        }

        if ( ! $this->allows_scheduled_vulnerability_refresh() ) {
            return;
        }

        $this->queue_vulnerability_refresh( 'asset_upgrade' );
    }

    private function allows_scheduled_vulnerability_refresh(): bool {
        if ( ! class_exists( '\SecurWP\Modules\Cloud\Service\Entitlements' ) ) {
            $path = __DIR__ . '/Service/Entitlements.php';
            if ( file_exists( $path ) ) {
                require_once $path;
            }
        }

        if ( ! class_exists( '\SecurWP\Modules\Cloud\Service\Entitlements' ) ) {
            return false;
        }

        return \SecurWP\Modules\Cloud\Service\Entitlements::allowsScheduledScans();
    }

    public function handle_scheduled_vulnerability_refresh(): void {
        $source = $this->get_pending_vulnerability_refresh_source();

        if ( ! $this->allows_scheduled_vulnerability_refresh() ) {
            $this->clear_pending_vulnerability_refresh();
            return;
        }

        if ( ! class_exists( '\SecurWP\Modules\Vulnerability\Services\VulnerabilityRefreshService' ) ) {
            $path = dirname( __DIR__ ) . '/Vulnerability/Services/VulnerabilityRefreshService.php';
            if ( file_exists( $path ) ) {
                require_once $path;
            }
        }

        if ( ! class_exists( '\SecurWP\Modules\Vulnerability\Services\VulnerabilityRefreshService' ) ) {
            securwp_log( 'SecurWP: Vulnerability refresh service class missing.' );
            return;
        }

        $result = ( new \SecurWP\Modules\Vulnerability\Services\VulnerabilityRefreshService() )->refresh();
        if ( is_wp_error( $result ) ) {
            $code = $result->get_error_code();
            if ( $code === 'vulnerability_scan_busy' ) {
                $this->mark_pending_vulnerability_refresh( $source );
                return;
            }
            if ( $code === 'vulnerability_scan_locked' ) {
                $this->mark_pending_vulnerability_refresh( $source );
                $this->queue_vulnerability_refresh( $source, self::VULNERABILITY_REFRESH_RETRY_DELAY );
                return;
            }
            $this->clear_pending_vulnerability_refresh();
            securwp_log(
                sprintf(
                    'SecurWP: Scheduled vulnerability refresh failed (%s): %s',
                    $source,
                    $result->get_error_message()
                )
            );
            return;
        }

        $this->clear_pending_vulnerability_refresh();
    }

    public function handle_scan_terminal_for_pending_vulnerability_refresh(): void {
        if ( ! $this->has_pending_vulnerability_refresh() || ! $this->allows_scheduled_vulnerability_refresh() ) {
            return;
        }

        $this->queue_vulnerability_refresh( $this->get_pending_vulnerability_refresh_source(), self::VULNERABILITY_REFRESH_DELAY );
    }

    private function queue_vulnerability_refresh( string $source, int $delay = self::VULNERABILITY_REFRESH_DELAY ): void {
        if ( ! $this->allows_scheduled_vulnerability_refresh() ) {
            return;
        }

        $this->mark_pending_vulnerability_refresh( $source );
        $run_at = time() + max( 0, $delay );

        if ( function_exists( '\\as_has_scheduled_action' ) && function_exists( '\\as_schedule_single_action' ) ) {
            if ( ! \as_has_scheduled_action( self::VULNERABILITY_REFRESH_HOOK, [], self::VULNERABILITY_REFRESH_GROUP ) ) {
                \as_schedule_single_action( $run_at, self::VULNERABILITY_REFRESH_HOOK, [], self::VULNERABILITY_REFRESH_GROUP );
            }
            return;
        }

        if ( ! wp_next_scheduled( self::VULNERABILITY_REFRESH_HOOK ) ) {
            wp_schedule_single_event( $run_at, self::VULNERABILITY_REFRESH_HOOK );
        }
    }

    private function mark_pending_vulnerability_refresh( string $source ): void {
        $source = sanitize_key( $source );
        if ( $source === '' ) {
            $source = 'scheduled';
        }
        update_option( self::VULNERABILITY_REFRESH_PENDING_OPTION, $source, false );
    }

    private function get_pending_vulnerability_refresh_source(): string {
        $source = (string) get_option( self::VULNERABILITY_REFRESH_PENDING_OPTION, '' );
        $source = sanitize_key( $source );
        return $source !== '' ? $source : 'scheduled';
    }

    private function has_pending_vulnerability_refresh(): bool {
        return get_option( self::VULNERABILITY_REFRESH_PENDING_OPTION, '' ) !== '';
    }

    private function clear_pending_vulnerability_refresh(): void {
        delete_option( self::VULNERABILITY_REFRESH_PENDING_OPTION );
    }


    public function get_license_manager(): LicenseManager {
        return $this->license_manager;
    }

    private function wp_error_to_payload( \WP_Error $error ): array {
        $data = $error->get_error_data();
        $payload = [
            'message' => $error->get_error_message(),
        ];
        if ( is_array( $data ) ) {
            if ( isset( $data['detail'] ) ) {
                $payload['detail'] = $data['detail'];
            }
            if ( isset( $data['response'] ) ) {
                $payload['response'] = $data['response'];
            }
            if ( isset( $data['status'] ) ) {
                $payload['status'] = (int) $data['status'];
            }
        }
        return $payload;
    }

    public function handle_ajax_request_otp() {
        check_ajax_referer( 'securwp_nonce', '_ajax_nonce' );
        
        if ( ! \SecurWP\Includes\Capabilities::can_manage_plugin() ) {
            wp_send_json_error( [ 'message' => 'Unauthorized' ] );
            return;
        }

        $email = sanitize_email( wp_unslash( $_POST['email'] ?? '' ) );
        if ( empty( $email ) ) {
            wp_send_json_error( [ 'message' => 'Email is required.' ] );
            return;
        }

        $result = $this->license_manager->request_otp( $email );

        if ( is_wp_error( $result ) ) {
            wp_send_json_error( $this->wp_error_to_payload( $result ) );
            return;
        }

        wp_send_json_success( $result );
    }

    public function handle_ajax_request_license_otp() {
        check_ajax_referer( 'securwp_nonce', '_ajax_nonce' );

        if ( ! \SecurWP\Includes\Capabilities::can_manage_plugin() ) {
            wp_send_json_error( [ 'message' => 'Unauthorized' ] );
            return;
        }

        $key = sanitize_text_field( wp_unslash( $_POST['license_key'] ?? '' ) );
        if ( empty( $key ) ) {
            wp_send_json_error( [ 'message' => 'License key is required.' ] );
            return;
        }

        $result = $this->license_manager->request_license_otp( $key );
        if ( is_wp_error( $result ) ) {
            wp_send_json_error( $this->wp_error_to_payload( $result ) );
            return;
        }

        wp_send_json_success( $result );
    }

    public function handle_ajax_verify_otp() {
        check_ajax_referer( 'securwp_nonce', '_ajax_nonce' );
        
        if ( ! \SecurWP\Includes\Capabilities::can_manage_plugin() ) {
            wp_send_json_error( [ 'message' => 'Unauthorized' ] );
            return;
        }

        $email = sanitize_email( wp_unslash( $_POST['email'] ?? '' ) );
        $otp = sanitize_text_field( wp_unslash( $_POST['otp'] ?? '' ) );
        if ( empty( $email ) || empty( $otp ) ) {
            wp_send_json_error( [ 'message' => 'Email and OTP are required.' ] );
            return;
        }

        $result = $this->license_manager->verify_otp( $email, $otp );

        if ( is_wp_error( $result ) ) {
            wp_send_json_error( $this->wp_error_to_payload( $result ) );
            return;
        }

        wp_send_json_success( $result );
    }

    public function handle_ajax_verify_license_key() {
        check_ajax_referer( 'securwp_nonce', '_ajax_nonce' );

        if ( ! \SecurWP\Includes\Capabilities::can_manage_plugin() ) {
            wp_send_json_error( [ 'message' => 'Unauthorized' ] );
            return;
        }

        $key = sanitize_text_field( wp_unslash( $_POST['license_key'] ?? '' ) );
        $otp = sanitize_text_field( wp_unslash( $_POST['otp'] ?? '' ) );
        $email = sanitize_email( wp_unslash( $_POST['email'] ?? '' ) );
        if ( empty( $key ) ) {
            wp_send_json_error( [ 'message' => 'License key is required.' ] );
            return;
        }
        if ( empty( $otp ) ) {
            wp_send_json_error( [ 'message' => 'OTP is required.' ] );
            return;
        }

        $result = $this->license_manager->verify_license( $key, $email, $otp );

        if ( is_wp_error( $result ) ) {
            wp_send_json_error( $this->wp_error_to_payload( $result ) );
            return;
        }

        wp_send_json_success( $result );
    }

    public function handle_ajax_console_connect_init() {
        check_ajax_referer( 'securwp_nonce', '_ajax_nonce' );

        if ( ! \SecurWP\Includes\Capabilities::can_manage_plugin() ) {
            wp_send_json_error( [ 'message' => 'Unauthorized' ] );
            return;
        }

        // Compute the exact settings page URL on this site; Console validates host + base path + /wp-admin prefix.
        $return_to = admin_url( 'admin.php?page=securwp-settings&tab=license' );

        // Preserve return_to_console param so the full round-trip (plugin → Console popup → plugin → Console) keeps the redirect target.
        $return_to_console = isset( $_POST['return_to_console'] ) ? esc_url_raw( wp_unslash( $_POST['return_to_console'] ) ) : '';
        if ( $return_to_console !== '' ) {
            $return_to = add_query_arg( 'securwp_return_to_console', rawurlencode( $return_to_console ), $return_to );
        }

        $result = $this->license_manager->console_connect_init( $return_to );
        if ( is_wp_error( $result ) ) {
            wp_send_json_error( $this->wp_error_to_payload( $result ) );
            return;
        }
        wp_send_json_success( $result );
    }

    public function handle_ajax_console_connect_exchange() {
        check_ajax_referer( 'securwp_nonce', '_ajax_nonce' );

        if ( ! \SecurWP\Includes\Capabilities::can_manage_plugin() ) {
            wp_send_json_error( [ 'message' => 'Unauthorized' ] );
            return;
        }

        $code = sanitize_text_field( wp_unslash( $_POST['code'] ?? '' ) );
        if ( empty( $code ) ) {
            wp_send_json_error( [ 'message' => 'Missing exchange code.' ] );
            return;
        }

        $result = $this->license_manager->console_connect_exchange( $code );
        if ( is_wp_error( $result ) ) {
            wp_send_json_error( $this->wp_error_to_payload( $result ) );
            return;
        }
        wp_send_json_success( $result );
    }

    public function handle_ajax_console_connect_preview() {
        check_ajax_referer( 'securwp_nonce', '_ajax_nonce' );

        if ( ! \SecurWP\Includes\Capabilities::can_manage_plugin() ) {
            wp_send_json_error( [ 'message' => 'Unauthorized' ] );
            return;
        }

        $code = sanitize_text_field( wp_unslash( $_POST['code'] ?? '' ) );
        if ( empty( $code ) ) {
            wp_send_json_error( [ 'message' => 'Missing exchange code.' ] );
            return;
        }

        $result = $this->license_manager->console_connect_preview( $code );
        if ( is_wp_error( $result ) ) {
            wp_send_json_error( $this->wp_error_to_payload( $result ) );
            return;
        }

        wp_send_json_success( $result );
    }

    public function handle_ajax_retry_pro_activation() {
        check_ajax_referer( 'securwp_nonce', '_ajax_nonce' );

        if ( ! \SecurWP\Includes\Capabilities::can_manage_plugin() ) {
            wp_send_json_error( [ 'message' => 'Unauthorized' ] );
            return;
        }

        $activation_session_token = sanitize_text_field( wp_unslash( $_POST['activation_session_token'] ?? '' ) );
        if ( empty( $activation_session_token ) ) {
            wp_send_json_error( [ 'message' => 'Activation session token is required.' ] );
            return;
        }

        $result = $this->license_manager->retry_pro_activation( $activation_session_token );
        if ( is_wp_error( $result ) ) {
            wp_send_json_error( $this->wp_error_to_payload( $result ) );
            return;
        }
        wp_send_json_success( $result );
    }

    public function handle_ajax_activate_free() {
        check_ajax_referer( 'securwp_nonce', '_ajax_nonce' );

        if ( ! \SecurWP\Includes\Capabilities::can_manage_plugin() ) {
            wp_send_json_error( [ 'message' => 'Unauthorized' ] );
            return;
        }

        $activation_session_token = sanitize_text_field( wp_unslash( $_POST['activation_session_token'] ?? '' ) );
        if ( empty( $activation_session_token ) ) {
            wp_send_json_error( [ 'message' => 'Activation session token is required.' ] );
            return;
        }

        $result = $this->license_manager->activate_free( $activation_session_token );
        if ( is_wp_error( $result ) ) {
            wp_send_json_error( $this->wp_error_to_payload( $result ) );
            return;
        }
        wp_send_json_success( $result );
    }

    public function handle_ajax_activate_pro() {
        check_ajax_referer( 'securwp_nonce', '_ajax_nonce' );

        if ( ! \SecurWP\Includes\Capabilities::can_manage_plugin() ) {
            wp_send_json_error( [ 'message' => 'Unauthorized' ] );
            return;
        }

        if ( $this->credential_guard_ajax() ) { return; }

        $result = $this->license_manager->activate_pro();

        if ( is_wp_error( $result ) ) {
            wp_send_json_error( $this->wp_error_to_payload( $result ) );
            return;
        }

        // Reload in-memory snapshot so $this->settings reflects the DB write
        // that LicenseManager::validate_license() performed via CloudSettings::save().
        $this->load_settings();
        $s = $this->settings;

        wp_send_json_success( [
            'message'            => (string) ( $result['message'] ?? 'Pro activation completed.' ),
            'license_status'     => (string) ( $s['license_status'] ?? '' ),
            'license_plan'       => (string) ( $s['license_plan'] ?? 'free' ),
            'license_key'        => CloudSettings::mask_license_key_for_display( (string) ( $s['license_key'] ?? '' ) ),
            'license_expires_at' => (int) ( $s['license_expires_at'] ?? 0 ),
            'upgrade_url'        => 'https://console.securewp.net/dashboard/billing',
        ] );
    }


    public function handle_ajax_deactivate() {
        check_ajax_referer( 'securwp_nonce', '_ajax_nonce' );
        if ( ! \SecurWP\Includes\Capabilities::can_manage_plugin() ) {
            wp_send_json_error( [ 'message' => 'Unauthorized' ] );
            return;
        }

        $this->license_manager->deactivate();

        wp_send_json_success( [
            'message' => 'License deactivated.',
            'status' => 'inactive',
            'plan' => 'free'
        ] );
    }

    public function handle_ajax_revalidate() {
        check_ajax_referer( 'securwp_nonce', '_ajax_nonce' );
        if ( ! \SecurWP\Includes\Capabilities::can_manage_plugin() ) {
            wp_send_json_error( [ 'message' => 'Unauthorized' ] );
            return;
        }

        if ( $this->credential_guard_ajax() ) { return; }

        $result = $this->license_manager->revalidate();

        if ( is_wp_error( $result ) ) {
            wp_send_json_error( $this->wp_error_to_payload( $result ) );
            return;
        }

        // Reload in-memory snapshot so $this->settings reflects the DB write
        // that LicenseManager::validate_license() performed via CloudSettings::save().
        $this->load_settings();
        $s = $this->settings;

        wp_send_json_success( [
            'message'            => 'License revalidated successfully.',
            'license_status'     => (string) ( $s['license_status'] ?? '' ),
            'license_plan'       => (string) ( $s['license_plan'] ?? 'free' ),
            'license_key'        => CloudSettings::mask_license_key_for_display( (string) ( $s['license_key'] ?? '' ) ),
            'license_expires_at' => (int) ( $s['license_expires_at'] ?? 0 ),
            'linked_record_url'  => (string) ( $s['linked_record_url'] ?? '' ),
            'migration_status'   => (string) ( $s['migration_status'] ?? 'none' ),
            'migration_pending_url' => (string) ( $s['migration_pending_url'] ?? '' ),
            'migration_replace_allowed' => (bool) ( $s['migration_replace_allowed'] ?? true ),
            'migration_replace_message' => (string) ( $s['migration_replace_message'] ?? '' ),
        ] );
    }

    public function handle_ajax_check() {
        check_ajax_referer( 'securwp_nonce', '_ajax_nonce' );
        if ( ! \SecurWP\Includes\Capabilities::can_manage_plugin() ) {
            wp_send_json_error( [ 'message' => 'Unauthorized' ] );
            return;
        }

        if ( $this->credential_guard_ajax() ) { return; }

        $this->license_manager->check_status();

        // Reload persisted settings to avoid returning stale values.
        $this->load_settings();

        wp_send_json_success( [
            'status' => (string) ( $this->settings['license_status'] ?? '' ),
            'plan' => (string) ( $this->settings['license_plan'] ?? 'free' ),
        ] );
    }

    public function maybe_auto_activate_from_constant(): void {
        if ( ! defined( 'SECURWP_LICENSE_KEY' ) ) {
            return;
        }
        // License-key activation now requires email OTP verification.
        // Prevent noisy retries from constant-based auto-activation.
        if ( ! get_transient( 'securwp_auto_activate_otp_notice' ) ) {
            securwp_log( '[SecurWP] SECURWP_LICENSE_KEY auto-activation is disabled. Complete activation with Email + OTP from SecurWP Settings.' );
            set_transient( 'securwp_auto_activate_otp_notice', '1', DAY_IN_SECONDS );
        }
    }

}
