<?php
/**
 * OnboardingAjax — AJAX handlers for the UltraGuard Pro setup wizard.
 *
 * Registered wp_ajax_ actions:
 *
 *   ultraguard_onboarding_get_state      — return current wizard state (modules + settings)
 *   ultraguard_onboarding_toggle_module  — activate or deactivate a module
 *   ultraguard_onboarding_save_profile   — persist site profile (site_type, admin_email)
 *   ultraguard_onboarding_apply_preset   — apply recommended settings for a site type
 *   ultraguard_onboarding_seed_baseline  — seed firewall rules + default DB data
 *   ultraguard_onboarding_save_license   — store licence key or record skip
 *   ultraguard_onboarding_complete       — mark wizard done, redirect to dashboard
 *   ultraguard_onboarding_dismiss        — dismiss without completing (sets complete flag)
 *
 * Security model (every handler):
 *   1. check_ajax_referer() against NONCE_ACTION.
 *   2. current_user_can( 'manage_options' ).
 *   3. Sanitise all inputs before use.
 *   4. wp_send_json_success / wp_send_json_error — never echo directly.
 *
 * @package UltraguardPro\modules\onboarding\ajax
 */

namespace UltraguardPro\modules\onboarding\ajax;

use UltraguardPro\core\DatabaseCrud;
use UltraguardPro\Plugin;
use UltraguardPro\modules\ModuleManager;
use UltraguardPro\modules\onboarding\OnboardingModule;
use UltraguardPro\support\AjaxResponseBuilder;
use UltraguardPro\support\LicenseInfo;

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

// phpcs:disable WordPress.Security.NonceVerification.Missing -- Onboarding AJAX handlers verify nonce centrally via $this->verify().

class OnboardingAjax {

    // ── Site-type → settings preset map ──────────────────────────────────────

    /**
     * Recommended settings for each site profile.
     *
     * Keys must match the option names in Settings::$default_settings.
     * Only the options that differ from the absolute defaults are listed —
     * the seeder does a wp_parse_args merge so unlisted keys keep their value.
     *
     * @var array<string, array<string, mixed>>
     */
    private const PRESETS = [
        'blog' => [
            'firewall_enabled'            => true,
            'firewall_mode'               => 'active',
            'login_limiter_enabled'       => true,
            'max_login_attempts'          => 5,
            'lockout_duration'            => 1800,
            'two_factor_auth'             => false,
            'strong_password_enforcement' => true,
            'hide_wp_version'             => true,
            'disable_xmlrpc'              => true,
            'security_headers'            => true,
            'log_admin_actions'           => true,
            'log_file_changes'            => true,
            'is_email_notifications'      => true,
            'critical_threat_notification'=> true,
            'auto_cleanup'                => true,
        ],
        'ecommerce' => [
            'firewall_enabled'            => true,
            'firewall_mode'               => 'active',
            'login_limiter_enabled'       => true,
            'max_login_attempts'          => 3,
            'lockout_duration'            => 3600,
            'lockout_duration_increment'  => true,
            'two_factor_auth'             => true,
            'strong_password_enforcement' => true,
            'ssl_enforcement'             => true,
            'hide_wp_version'             => true,
            'disable_xmlrpc'              => true,
            'security_headers'            => true,
            'content_security_policy'     => true,
            'block_tor_users'             => true,
            'block_proxy_users'           => true,
            'rate_limiting_enabled'       => true,
            'rate_limit_requests'         => 60,
            'database_security'           => true,
            'log_admin_actions'           => true,
            'log_file_changes'            => true,
            'log_successful_logins'       => true,
            'is_email_notifications'      => true,
            'critical_threat_notification'=> true,
            'notify_lockouts'             => true,
            'auto_cleanup'                => true,
            'backup_before_actions'       => true,
        ],
        'agency' => [
            'firewall_enabled'            => true,
            'firewall_mode'               => 'active',
            'login_limiter_enabled'       => true,
            'max_login_attempts'          => 4,
            'lockout_duration'            => 2700,
            'two_factor_auth'             => true,
            'strong_password_enforcement' => true,
            'hide_wp_version'             => true,
            'disable_xmlrpc'              => true,
            'security_headers'            => true,
            'detailed_logging'            => true,
            'log_admin_actions'           => true,
            'log_file_changes'            => true,
            'log_successful_logins'       => true,
            'is_email_notifications'      => true,
            'critical_threat_notification'=> true,
            'network_wide_protection'     => false, // user chooses
            'auto_cleanup'                => true,
            'backup_before_actions'       => true,
        ],
        'custom' => [
            // Minimal safe baseline — user configures the rest manually.
            'firewall_enabled'            => true,
            'firewall_mode'               => 'learning',
            'login_limiter_enabled'       => true,
            'hide_wp_version'             => true,
            'security_headers'            => true,
            'is_email_notifications'      => true,
        ],
    ];

    /**
     * Built-in modules that should never appear in the onboarding selector.
     *
     * Compatibility modules stay hidden so onboarding only reflects the live
     * product surface.
     *
     * @var string[]
     */
    private const EXCLUDED_MODULES = [
        'login_protect',
    ];

    // ── Constructor ───────────────────────────────────────────────────────────

    private Plugin $plugin;

    public function __construct( Plugin $plugin ) {
        $this->plugin = $plugin;
    }

    // ── Registration ──────────────────────────────────────────────────────────

    public function register(): void {
        $actions = [
            'ultraguard_onboarding_get_state'     => 'handle_get_state',
            'ultraguard_onboarding_toggle_module'  => 'handle_toggle_module',
            'ultraguard_onboarding_save_profile'   => 'handle_save_profile',
            'ultraguard_onboarding_apply_preset'   => 'handle_apply_preset',
            'ultraguard_onboarding_seed_baseline'  => 'handle_seed_baseline',
            'ultraguard_onboarding_save_license'   => 'handle_save_license',
            'ultraguard_onboarding_complete'       => 'handle_complete',
            'ultraguard_onboarding_dismiss'        => 'handle_dismiss',
        ];

        foreach ( $actions as $action => $method ) {
            add_action( 'wp_ajax_' . $action, [ $this, $method ] );
        }
    }

    // =========================================================================
    // Handlers
    // =========================================================================

    /**
     * GET STATE — return everything the wizard JS needs to bootstrap itself.
     */
    public function handle_get_state(): void {
        $this->verify();

        $is_pro = LicenseInfo::instance()->isPro();

        $modules = [];
        foreach ( $this->get_selectable_modules() as $id => $m ) {
            $modules[] = [
                'id'               => $id,
                'name'             => $m['name']             ?? $id,
                'description'      => $m['description']      ?? '',
                'icon'             => $m['icon']             ?? 'dashicons-admin-plugins',
                'is_active'        => $m['is_active']        ?? false,
                'license_required' => $m['license_required'] ?? 'free',
                'license_ok'       => $m['license_ok']       ?? true,
                'upgrade_url'      => $m['upgrade_url']      ?? '',
            ];
        }

        $this->send_success( [
            'step'              => (int) get_option( OnboardingModule::STEP_OPTION, 1 ),
            'is_complete'       => OnboardingModule::is_complete(),
            'is_pro'            => $is_pro,
            'admin_email'       => get_option( 'admin_email', '' ),
            'site_name'         => get_bloginfo( 'name' ),
            'site_type'         => get_option( 'ultraguard_onboarding_site_type', '' ),
            'skipped_license'   => (bool) get_option( 'ultraguard_onboarding_skipped_license', false ),
            'modules'           => $modules,
            'dashboard_url'     => admin_url( 'admin.php?page=ultraguard-security-suite-dashboard' ),
            'upgrade_url'       => admin_url( 'admin.php?page=ultraguard-upgrade' ),
            'presets'           => array_keys( self::PRESETS ),
        ] );
    }

    // ── Step 2 — Module selection ─────────────────────────────────────────────

    /**
     * Toggle a module on or off from the wizard's module-selection step.
     */
    public function handle_toggle_module(): void {
        $this->verify();

        $module_id = sanitize_key( wp_unslash( $_POST['module_id'] ?? '' ) );
        $activate  = filter_var( wp_unslash( $_POST['activate'] ?? true ), FILTER_VALIDATE_BOOLEAN );

        if ( empty( $module_id ) ) {
            $this->send_error( __( 'Module ID is required.', 'ultraguard-security-suite' ) );
        }

        // Prevent toggling of core / undiscoverable modules.
        if ( ! in_array( $module_id, $this->get_selectable_module_ids(), true ) ) {
            $this->send_error( __( 'This module cannot be changed from the wizard.', 'ultraguard-security-suite' ) );
        }

        $manager = $this->get_manager();
        $result  = $activate ? $manager->activate( $module_id ) : $manager->deactivate( $module_id );

        if ( ! $result['success'] ) {
            $this->send_error( (string) ( $result['message'] ?? __( 'Module update failed.', 'ultraguard-security-suite' ) ), 400, (string) ( $result['code'] ?? 'error' ), $result );
        }

        $this->send_success( array_merge( $result, [ 'module_id' => $module_id, 'is_active' => $activate ] ) );
    }

    // ── Step 1 — Profile ──────────────────────────────────────────────────────

    /**
     * Persist the admin e-mail and site-type profile chosen on step 1.
     */
    public function handle_save_profile(): void {
        $this->verify();

        $admin_email = sanitize_email( wp_unslash( $_POST['admin_email'] ?? '' ) );
        $site_type   = sanitize_key( wp_unslash( $_POST['site_type']   ?? 'custom' ) );

        if ( ! array_key_exists( $site_type, self::PRESETS ) ) {
            $site_type = 'custom';
        }

        if ( $admin_email && is_email( $admin_email ) ) {
            update_option( 'admin_email', $admin_email );
            $this->plugin->settings->update_option( 'admin_email', $admin_email );
        }

        update_option( 'ultraguard_onboarding_site_type', $site_type );
        update_option( OnboardingModule::STEP_OPTION, max( 1, (int) get_option( OnboardingModule::STEP_OPTION, 1 ) ) );

        $this->send_success( [
            'message'   => __( 'Profile saved.', 'ultraguard-security-suite' ),
            'site_type' => $site_type,
        ] );
    }

    // ── Step 3 — Settings preset ──────────────────────────────────────────────

    /**
     * Apply the recommended settings preset for the chosen site type.
     */
    public function handle_apply_preset(): void {
        $this->verify();

        $site_type = sanitize_key( wp_unslash( $_POST['site_type'] ?? '' ) );

        if ( ! isset( self::PRESETS[ $site_type ] ) ) {
            $this->send_error( __( 'Unknown site type.', 'ultraguard-security-suite' ) );
        }

        $preset  = self::PRESETS[ $site_type ];
        $applied = [];
        $failed  = [];

        foreach ( $preset as $key => $value ) {
            $ok = $this->plugin->settings->update_option( $key, $value );
            if ( $ok ) {
                $applied[] = $key;
            } else {
                $failed[] = $key;
            }
        }

        update_option( 'ultraguard_onboarding_site_type', $site_type );
        update_option( OnboardingModule::STEP_OPTION, max( 3, (int) get_option( OnboardingModule::STEP_OPTION, 1 ) ) );

        $this->send_success( [
            'message'   => sprintf(
                /* translators: %d: number of settings applied */
                __( '%d settings applied for your site profile.', 'ultraguard-security-suite' ),
                count( $applied )
            ),
            'applied'   => $applied,
            'failed'    => $failed,
            'site_type' => $site_type,
        ] );
    }

    // ── Step 4 — Baseline seeding ─────────────────────────────────────────────

    /**
     * Seed baseline data into the database.
     *
     * This is an idempotent operation — calling it multiple times is safe.
     * Each seeder checks whether data already exists before inserting.
     *
     * The handler streams back a tasks array so the wizard's progress bar can
     * tick through each item in sequence via repeated AJAX calls when the
     * client passes task_index=N.
     */
    public function handle_seed_baseline(): void {
        $this->verify();

        $task_index = absint( wp_unslash( $_POST['task_index'] ?? 0 ) );

        $tasks = $this->get_baseline_tasks();

        if ( ! isset( $tasks[ $task_index ] ) ) {
            // All tasks done.
            update_option( OnboardingModule::STEP_OPTION, max( 4, (int) get_option( OnboardingModule::STEP_OPTION, 1 ) ) );
            $this->send_success( [
                'done'       => true,
                'message'    => __( 'Baseline setup complete.', 'ultraguard-security-suite' ),
                'task_index' => $task_index,
                'total'      => count( $tasks ),
            ] );
        }

        $task   = $tasks[ $task_index ];
        $result = $this->run_baseline_task( $task );

        $this->send_success( [
            'done'        => false,
            'task_index'  => $task_index,
            'next_index'  => $task_index + 1,
            'total'       => count( $tasks ),
            'task_label'  => $task['label'],
            'task_result' => $result,
        ] );
    }

    // ── Step 5 — Licence ──────────────────────────────────────────────────────

    /**
     * Persist or validate a licence key; or record that the user chose to skip.
     */
    public function handle_save_license(): void {
        $this->verify();

        $license_key = sanitize_text_field( wp_unslash( $_POST['license_key'] ?? '' ) );
        $skipped     = filter_var( wp_unslash( $_POST['skipped'] ?? false ), FILTER_VALIDATE_BOOLEAN );

        if ( $skipped ) {
            update_option( 'ultraguard_onboarding_skipped_license', true );

            $this->send_success( [
                'message'          => __( 'Setup continued without a licence. Note: free users receive security updates within 24 hours. Pro users receive them instantly.', 'ultraguard-security-suite' ),
                'is_pro'           => false,
                'skipped'          => true,
                'delay_warning'    => true,
                'delay_days'       => 1,
            ] );
        }

        if ( empty( $license_key ) ) {
            $this->send_error( __( 'Please enter a licence key or choose to skip.', 'ultraguard-security-suite' ) );
        }

        // Delegate actual key validation to the existing LicenseInfo service.
        $license = LicenseInfo::instance();

        if ( method_exists( $license, 'activate_license' ) ) {
            $activation = $license->activate_license( $license_key );

            if ( is_wp_error( $activation ) ) {
                $this->send_error( (string) $activation->get_error_message(), 400, (string) $activation->get_error_code(), [
                    'message'     => $activation->get_error_message(),
                    'error_code'  => $activation->get_error_code(),
                ] );
            }

            $this->send_success( [
                'message' => __( 'Licence activated successfully. You now have access to all Pro features and real-time updates.', 'ultraguard-security-suite' ),
                'is_pro'  => $license->isPro(),
                'skipped' => false,
            ] );
        }

        // Fallback — store the key and let the cron checker verify it later.
        update_option( 'ultraguard_license_key', $license_key );
        delete_option( 'ultraguard_onboarding_skipped_license' );

        $this->send_success( [
            'message' => __( 'Licence key saved. Validation will complete in the background.', 'ultraguard-security-suite' ),
            'is_pro'  => false,
            'skipped' => false,
            'pending' => true,
        ] );
    }

    // ── Final step — Complete / Dismiss ───────────────────────────────────────

    /**
     * Mark the wizard as complete and return the dashboard redirect URL.
     */
    public function handle_complete(): void {
        $this->verify();

        OnboardingModule::mark_complete();

        do_action( 'ultraguard_pro/onboarding/completed' );

        $this->send_success( [
            'message'      => __( 'Setup complete! Redirecting to your security dashboard…', 'ultraguard-security-suite' ),
            'redirect_url' => admin_url( 'admin.php?page=ultraguard-security-suite-dashboard' ),
        ] );
    }

    /**
     * Dismiss the wizard without stepping through it; still marks as complete
     * so the redirect guard never fires again.
     */
    public function handle_dismiss(): void {
        $this->verify();

        OnboardingModule::mark_complete();

        do_action( 'ultraguard_pro/onboarding/dismissed' );

        $this->send_success( [
            'message'      => __( 'Wizard dismissed.', 'ultraguard-security-suite' ),
            'redirect_url' => admin_url( 'admin.php?page=ultraguard-security-suite-dashboard' ),
        ] );
    }

    // =========================================================================
    // Baseline tasks
    // =========================================================================

    /**
     * Ordered list of baseline seeding tasks.
     *
     * Each entry: [ 'label' => string (shown in the wizard), 'task' => string (internal key) ]
     *
     * @return array<int, array{label: string, task: string}>
     */
    private function get_baseline_tasks(): array {
        return [
            [ 'label' => __( 'Applying default settings…',         'ultraguard-security-suite' ), 'task' => 'default_settings'   ],
            [ 'label' => __( 'Seeding firewall rules…',            'ultraguard-security-suite' ), 'task' => 'firewall_rules'     ],
            [ 'label' => __( 'Seeding attack pattern signatures…', 'ultraguard-security-suite' ), 'task' => 'attack_patterns'    ],
            [ 'label' => __( 'Configuring login protection…',      'ultraguard-security-suite' ), 'task' => 'login_protection'   ],
            [ 'label' => __( 'Setting up security headers…',       'ultraguard-security-suite' ), 'task' => 'security_headers'   ],
            [ 'label' => __( 'Initialising audit log…',            'ultraguard-security-suite' ), 'task' => 'audit_log'          ],
            [ 'label' => __( 'Configuring notification channels…', 'ultraguard-security-suite' ), 'task' => 'notifications'      ],
            [ 'label' => __( 'Finalising database tables…',        'ultraguard-security-suite' ), 'task' => 'db_finalize'        ],
        ];
    }

    /**
     * Execute a single baseline task and return a result summary string.
     *
     * @param  array{label: string, task: string} $task
     * @return string  Human-readable result.
     */
    private function run_baseline_task( array $task ): string {
        switch ( $task['task'] ) {

            case 'default_settings':
                $ok = $this->plugin->settings->set_default_options();
                \UltraguardPro\ultraguard_pro_seed_default_module_settings();
                return $ok
                    ? __( 'Default settings and module defaults applied.', 'ultraguard-security-suite' )
                    : __( 'Settings already configured; missing module defaults were seeded.', 'ultraguard-security-suite' );

            case 'firewall_rules':
                return $this->seed_firewall_rules();

            case 'attack_patterns':
                return $this->seed_attack_patterns();

            case 'login_protection':
                $this->plugin->settings->update_option( 'login_limiter_enabled', true );
                $this->plugin->settings->update_option( 'max_login_attempts', 5 );
                $this->plugin->settings->update_option( 'lockout_duration', 1800 );
                return __( 'Login protection configured.', 'ultraguard-security-suite' );

            case 'security_headers':
                $this->plugin->settings->update_option( 'security_headers', true );
                $this->plugin->settings->update_option( 'hide_wp_version', true );
                $this->plugin->settings->update_option( 'remove_wp_generator', true );
                return __( 'Security headers enabled.', 'ultraguard-security-suite' );

            case 'audit_log':
                $this->plugin->settings->update_option( 'log_admin_actions', true );
                $this->plugin->settings->update_option( 'log_file_changes', true );
                $this->plugin->settings->update_option( 'detailed_logging', true );
                return __( 'Audit logging activated.', 'ultraguard-security-suite' );

            case 'notifications':
                $admin_email = get_option( 'admin_email', '' );
                if ( $admin_email ) {
                    $this->plugin->settings->update_option( 'is_email_notifications', true );
                    $this->plugin->settings->update_option( 'admin_email', $admin_email );
                    $this->plugin->settings->update_option( 'critical_threat_notification', true );
                }
                return __( 'Notification channels configured.', 'ultraguard-security-suite' );

            case 'db_finalize':
                // Trigger any pending DB upgrades.
                delete_option( 'ultraguard_pro_db_version' );
                do_action( 'ultraguard_pro/onboarding/baseline_complete' );
                return __( 'Database tables verified.', 'ultraguard-security-suite' );

            default:
                return __( 'Task skipped (unknown).', 'ultraguard-security-suite' );
        }
    }

    /**
     * Seed the built-in firewall rules from the bundled JSON file if the table
     * is empty.  Uses the FirewallSeeder when available; falls back to direct
     * table inserts via the BaseTable API.
     *
     * @return string Result description.
     */
    private function seed_firewall_rules(): string {
        // Use the existing FirewallSeeder if loaded.
        if ( class_exists( '\UltraguardPro\modules\firewall\FirewallSeeder' ) ) {
            try {
                $seeder = new \UltraguardPro\modules\firewall\FirewallSeeder();
                $seeder->seed_rules();
                return __( 'Firewall rules seeded via FirewallSeeder.', 'ultraguard-security-suite' );
            } catch ( \Throwable $e ) {
                // Fall through to direct insert below.
            }
        }

        $count = (int) DatabaseCrud::get_count( 'ultraguard_firewall_rules' );

        if ( $count > 0 ) {
            return __( 'Firewall rules already present — skipped.', 'ultraguard-security-suite' );
        }

        $json_path = ULTRAGUARD_PRO_PATH . 'assets/json/firewall-rules.json';
        if ( ! file_exists( $json_path ) ) {
            return __( 'Firewall rules JSON not found — skipped.', 'ultraguard-security-suite' );
        }

        $rules = json_decode( file_get_contents( $json_path ), true ); // phpcs:ignore
        if ( ! is_array( $rules ) ) {
            return __( 'Firewall rules JSON malformed — skipped.', 'ultraguard-security-suite' );
        }

        if ( isset( $rules['rules'] ) && is_array( $rules['rules'] ) ) {
            $rules = $rules['rules'];
        }

        $inserted = 0;
        foreach ( $rules as $rule ) {
            if ( empty( $rule['name'] ) ) {
                continue;
            }
            $ok = DatabaseCrud::insert( 'ultraguard_firewall_rules', [
                'name'           => sanitize_text_field( $rule['name'] ),
                'description'    => sanitize_text_field( $rule['description'] ?? '' ),
                'rule_type'      => sanitize_text_field( $rule['rule_type'] ?? 'block' ),
                'match_field'    => sanitize_text_field( $rule['match_field'] ?? 'uri' ),
                'match_operator' => sanitize_text_field( $rule['match_operator'] ?? 'contains' ),
                'match_value'    => (string) ( $rule['match_value'] ?? '' ),
                'action'         => sanitize_text_field( $rule['action'] ?? 'block' ),
                'is_active'      => ! empty( $rule['is_active'] ) ? 1 : 0,
                'priority'       => (int) ( $rule['priority'] ?? 10 ),
                'created_by'     => get_current_user_id() ?: null,
                'created_at'     => current_time( 'mysql', true ),
            ] );
            if ( $ok ) {
                $inserted++;
            }
        }

        return sprintf(
            /* translators: %d: number of rules inserted */
            __( '%d firewall rules inserted.', 'ultraguard-security-suite' ),
            $inserted
        );
    }

    /**
     * Seed attack-pattern signatures if the table is empty.
     *
     * @return string Result description.
     */
    private function seed_attack_patterns(): string {
        if ( class_exists( '\UltraguardPro\modules\firewall\FirewallSeeder' ) ) {
            try {
                $seeder = new \UltraguardPro\modules\firewall\FirewallSeeder();
                $seeder->seed_patterns();
                return __( 'Attack patterns seeded via FirewallSeeder.', 'ultraguard-security-suite' );
            } catch ( \Throwable $e ) {
                // Fall through.
            }
        }

        $count = (int) DatabaseCrud::get_count( 'ultraguard_attack_patterns' );

        if ( $count > 0 ) {
            return __( 'Attack-pattern signatures already present — skipped.', 'ultraguard-security-suite' );
        }

        $json_path = ULTRAGUARD_PRO_PATH . 'assets/json/attack-patterns.json';
        if ( ! file_exists( $json_path ) ) {
            return __( 'Attack-patterns JSON not found — skipped.', 'ultraguard-security-suite' );
        }

        $patterns = json_decode( file_get_contents( $json_path ), true ); // phpcs:ignore
        if ( ! is_array( $patterns ) ) {
            return __( 'Attack-patterns JSON malformed — skipped.', 'ultraguard-security-suite' );
        }

        if ( isset( $patterns['patterns'] ) && is_array( $patterns['patterns'] ) ) {
            $patterns = $patterns['patterns'];
        }

        $inserted = 0;
        foreach ( $patterns as $pattern ) {
            if ( empty( $pattern['pattern_name'] ) || empty( $pattern['pattern_value'] ) ) {
                continue;
            }
            $ok = DatabaseCrud::insert( 'ultraguard_attack_patterns', [
                'pattern_name'    => sanitize_text_field( $pattern['pattern_name'] ),
                'pattern_type'    => sanitize_text_field( $pattern['pattern_type'] ?? 'regex' ),
                'pattern_value'   => (string) $pattern['pattern_value'],
                'attack_category' => sanitize_text_field( $pattern['attack_category'] ?? '' ),
                'severity'        => sanitize_text_field( $pattern['severity'] ?? 'medium' ),
                'is_active'       => 1,
                'created_at'      => current_time( 'mysql', true ),
            ] );
            if ( $ok ) {
                $inserted++;
            }
        }

        return sprintf(
            /* translators: %d: number of signatures inserted */
            __( '%d attack-pattern signatures inserted.', 'ultraguard-security-suite' ),
            $inserted
        );
    }

    // =========================================================================
    // Private helpers
    // =========================================================================

    /**
     * Verify nonce and capability for every AJAX handler.
     * Sends a JSON error and exits on failure — does NOT return false.
     */
    private function verify(): void {
        if ( ! check_ajax_referer( OnboardingModule::NONCE_ACTION, 'nonce', false ) ) {
            AjaxResponseBuilder::make()
                ->nonceError( __( 'Security check failed.', 'ultraguard-security-suite' ) )
                ->send( 403 );
        }

        if ( ! \UltraguardPro\ultraguard_current_user_can_manage() ) {
            AjaxResponseBuilder::make()
                ->permissionError( __( 'Permission denied.', 'ultraguard-security-suite' ) )
                ->send( 403 );
        }
    }

    /**
     * Send a successful onboarding response through the shared AJAX manager.
     *
     * @param array<string,mixed> $data
     */
    private function send_success( array $data ): void {
        $message  = isset( $data['message'] ) ? (string) $data['message'] : '';
        $redirect = isset( $data['redirect_url'] ) && is_string( $data['redirect_url'] )
            ? $data['redirect_url']
            : ( ( isset( $data['redirect'] ) && is_string( $data['redirect'] ) ) ? $data['redirect'] : '' );

        $builder = AjaxResponseBuilder::make()->success( $message, $data );

        if ( '' !== $message ) {
            $builder->addNotification( $message, 'success' );
        }

        if ( '' !== $redirect ) {
            $builder->addRedirect( $redirect, 300 );
        }

        $builder->send();
    }

    /**
     * Send an onboarding error through the shared AJAX manager.
     *
     * @param string              $message
     * @param int                 $status
     * @param string              $code
     * @param array<string,mixed> $data
     */
    private function send_error( string $message, int $status = 400, string $code = 'error', array $data = [] ): void {
        AjaxResponseBuilder::make()
            ->error( $message, $code, $data )
            ->addNotification( $message, 'error' )
            ->send( $status );
    }

    /**
     * Return the ModuleManager instance from the plugin container.
     *
     * @return ModuleManager
     */
    private function get_manager(): ModuleManager {
        return new ModuleManager( $this->plugin );
    }

    /**
     * Current feature modules shown in the onboarding module selection step.
     *
     * @return array<string, array<string, mixed>>
     */
    private function get_selectable_modules(): array {
        $modules = [];

        foreach ( $this->get_manager()->get_all_modules() as $id => $manifest ) {
            if ( 'feature' !== ( $manifest['type'] ?? 'feature' ) ) {
                continue;
            }

            if ( in_array( $id, self::EXCLUDED_MODULES, true ) ) {
                continue;
            }

            $modules[ $id ] = $manifest;
        }

        uasort(
            $modules,
            static function ( array $a, array $b ): int {
                return (int) ( $a['priority'] ?? 50 ) <=> (int) ( $b['priority'] ?? 50 );
            }
        );

        return $modules;
    }

    /**
     * @return string[]
     */
    private function get_selectable_module_ids(): array {
        return array_keys( $this->get_selectable_modules() );
    }
}

// phpcs:enable
