<?php
/**
 * LoginLimiterAjax — All wp_ajax_{action} handlers for the Login Limiter module.
 *
 * Security layers on EVERY handler:
 *   1. check_ajax_referer( NONCE, 'nonce' )  — CSRF
 *   2. current_user_can( 'manage_options' )   — privilege
 *
 * All actions use the shared nonce action: 'ultraguard_admin_nonce'.
 * Action names use the 'ultraguard_ll_' prefix to avoid collisions.
 *
 * Self-containment: this class only accesses tables registered by
 * LoginLimiterSchema. It does NOT touch the Firewall module's ip_blacklist
 * or ip_whitelist tables.
 *
 * @package UltraguardPro\modules\loginlimiter\ajax
 * @since   6.5.0
 */

namespace UltraguardPro\modules\loginlimiter\ajax;

use UltraguardPro\Plugin;
use UltraguardPro\modules\loginlimiter\LoginLimiterEngine;
use UltraguardPro\modules\core\ModuleAjaxSettingsTrait;
use UltraguardPro\support\AjaxResponseBuilder;

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

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

class LoginLimiterAjax {

    use ModuleAjaxSettingsTrait;

    /** Shared nonce action for all Login Limiter AJAX calls. */
    private const NONCE = 'ultraguard_admin_nonce';

    /** @var Plugin */
    private Plugin $plugin;

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

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

    public function register(): void {
        $map = [
            // ── Dashboard ──────────────────────────────────────────────────────
            'ultraguard_ll_refresh_stats'       => 'handle_refresh_stats',
            'ultraguard_ll_update_chart_period' => 'handle_update_chart_period',
            'ultraguard_ll_clear_attempts'      => 'handle_clear_attempts',

            // ── Login Attempts ─────────────────────────────────────────────────
            'ultraguard_ll_get_attempts'        => 'handle_get_attempts',
            'ultraguard_ll_export_attempts'     => 'handle_export_attempts',
            'ultraguard_ll_filter_attempts'     => 'handle_filter_attempts',
            'ultraguard_ll_block_attempt_ip'    => 'handle_block_attempt_ip',
            'ultraguard_ll_allow_attempt_ip'    => 'handle_allow_attempt_ip',

            // ── IP Management ──────────────────────────────────────────────────
            'ultraguard_ll_block_ip'            => 'handle_block_ip',
            'ultraguard_ll_allow_ip'            => 'handle_allow_ip',
            'ultraguard_ll_remove_blocked_ip'   => 'handle_remove_blocked_ip',
            'ultraguard_ll_remove_allowed_ip'   => 'handle_remove_allowed_ip',
            'ultraguard_ll_bulk_block_ips'      => 'handle_bulk_block_ips',
            'ultraguard_ll_bulk_allow_ips'      => 'handle_bulk_allow_ips',
            'ultraguard_ll_bulk_remove_blocked' => 'handle_bulk_remove_blocked',
            'ultraguard_ll_import_ips'          => 'handle_import_ips',
            'ultraguard_ll_export_ips'          => 'handle_export_ips',
            'ultraguard_ll_validate_ips'        => 'handle_validate_ips',
            'ultraguard_ll_view_ip_details'     => 'handle_view_ip_details',
            'ultraguard_ll_refresh_ip_stats'    => 'handle_refresh_ip_stats',
            'ultraguard_ll_clear_expired_ips'   => 'handle_clear_expired_ips',

            // ── Lockouts ───────────────────────────────────────────────────────
            'ultraguard_ll_unlock_ip'           => 'handle_unlock_ip',
            'ultraguard_ll_unlock_all'          => 'handle_unlock_all',
            'ultraguard_ll_bulk_unlock'         => 'handle_bulk_unlock',
            'ultraguard_ll_bulk_blacklist'      => 'handle_bulk_blacklist',
            'ultraguard_ll_bulk_allowlist'      => 'handle_bulk_allowlist',
            'ultraguard_ll_export_lockouts'     => 'handle_export_lockouts',
            'ultraguard_ll_clear_expired'       => 'handle_clear_expired_lockouts',
            'ultraguard_ll_refresh_lockouts'    => 'handle_refresh_lockouts',

            // ── Settings ───────────────────────────────────────────────────────
            'ultraguard_ll_save_settings'       => 'handle_save_settings',
            'ultraguard_ll_test_protection'     => 'handle_test_protection',
            'ultraguard_ll_security_audit'      => 'handle_security_audit',
            'ultraguard_ll_export_audit'        => 'handle_export_audit',
            'ultraguard_ll_reset_stats'         => 'handle_reset_stats',
            'ultraguard_ll_clear_lockouts'      => 'handle_unlock_all',

            // ── Row-level record mutations (table action buttons) ──────────────────
            'ultraguard_ll_delete_attempt'      => 'handle_delete_attempt',
            'ultraguard_ll_edit_blocked_ip'     => 'handle_edit_blocked_ip',
            'ultraguard_ll_edit_allowed_ip'     => 'handle_edit_allowed_ip',
            'ultraguard_ll_delete_lockout'      => 'handle_delete_lockout',

            // ── Sessions ───────────────────────────────────────────────────────
            'ultraguard_ll_get_sessions'        => 'handle_get_sessions',
            'ultraguard_ll_revoke_session'      => 'handle_revoke_session',
            'ultraguard_ll_revoke_user_sessions'=> 'handle_revoke_user_sessions',
            // ── Realtime fallback polling ─────────────────────────────────────
            'ultraguard_rt_login_status'        => 'handle_rt_login_status',
        ];

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

    // ── Dashboard ──────────────────────────────────────────────────────────────

    public function handle_refresh_stats(): void {
        $this->verify();

        $att  = $this->t( 'login_attempts' );
        $lo   = $this->t( 'login_limiter_lockouts' );
        $sess = $this->t( 'user_sessions' );
        $bl   = $this->t( 'll_blocked_ips' );

        $today = gmdate( 'Y-m-d 00:00:00' );
        $h24   = gmdate( 'Y-m-d H:i:s', strtotime( '-24 hours' ) );

        $failed_today    = $att  ? $att->count(  [ 'status' => 'failed',  'created_at >=' => $today ] ) : 0;
        $success_today   = $att  ? $att->count(  [ 'status' => 'success', 'created_at >=' => $today ] ) : 0;
        $active_lockouts = $lo   ? $lo->count(   [ 'is_active' => 1 ] )                                  : 0;
        $active_sessions = $sess ? $sess->count()                                                         : 0;
        $blocked_ips     = $bl   ? $bl->count(   [ 'is_active' => 1 ] )                                  : 0;

        AjaxResponseBuilder::make()
            ->success( __( 'Stats refreshed.', 'ultraguard-security-suite' ) )
            ->addData( 'failed_today',    $failed_today )
            ->addData( 'success_today',   $success_today )
            ->addData( 'active_lockouts', $active_lockouts )
            ->addData( 'active_sessions', $active_sessions )
            ->addData( 'blocked_ips',     $blocked_ips )
            ->addData( 'chart_data', $att ? $att->get_chart_data( 'created_at', 'id', 'hour', [ 'status' => 'failed' ], 24 ) : [] )
            ->updateText( '#ultraguard-ll-stat-failed-today',    (string) $failed_today )
            ->updateText( '#ultraguard-ll-stat-success-today',   (string) $success_today )
            ->updateText( '#ultraguard-ll-stat-active-lockouts', (string) $active_lockouts )
            ->updateText( '#ultraguard-ll-stat-active-sessions', (string) $active_sessions )
            ->updateText( '#ultraguard-ll-stat-blocked-ips',     (string) $blocked_ips )
            ->send();
    }

    public function handle_update_chart_period(): void {
        $this->verify();
        $period = absint( $_POST['period'] ?? 24 );
        $period = in_array( $period, [ 1, 6, 12, 24, 48, 168 ], true ) ? $period : 24;
        $group  = $period <= 24 ? 'hour' : 'day';
        $att    = $this->t( 'login_attempts' );

        AjaxResponseBuilder::make()
            ->success()
            ->addData( 'chart_data', $att ? $att->get_chart_data( 'created_at', 'id', $group, [ 'status' => 'failed' ], $period ) : [] )
            ->addData( 'period', $period )
            ->send();
    }

    public function handle_clear_attempts(): void {
        $this->verify();
        $t = $this->t( 'login_attempts' );
        if ( $t ) { $t->truncate(); }
        AjaxResponseBuilder::make()
            ->success( __( 'Login attempt logs cleared.', 'ultraguard-security-suite' ) )
            ->addNotification( __( 'Cleared!', 'ultraguard-security-suite' ), 'success' )
            ->emptyElement( '#ultraguard-ll-attempts-table tbody' )
            ->updateText( '#ultraguard-ll-attempts-count', '0' )
            ->send();
    }

    // ── Login Attempts ─────────────────────────────────────────────────────────

    public function handle_get_attempts(): void {
        $this->verify();
        $page   = max( 1, absint( $_POST['page'] ?? 1 ) );
        $status = sanitize_key( wp_unslash( $_POST['status'] ?? ''  ) );
        $ip     = sanitize_text_field( wp_unslash( $_POST['filter_ip'] ?? '' ) );
        $conds  = [];
        if ( $status ) { $conds['status']     = $status; }
        if ( $ip )     { $conds['ip_address'] = $ip; }
        $t = $this->t( 'login_attempts' );
        AjaxResponseBuilder::make()->success()->mergeData( (array) ( $t ? $t->paginate( $page, 50, $conds, 'created_at', 'DESC' ) : [ 'records' => [], 'total' => 0 ] ) )->send();
    }

    public function handle_export_attempts(): void {
        $this->verify();
        $t   = $this->t( 'login_attempts' );
        $csv = $t ? $t->export_to_csv(
            [],
            [ 'ip_address', 'username', 'status', 'country_code', 'user_agent', 'created_at' ],
            100000
        ) : '';

        $url = $this->save_export( 'login-attempts-' . gmdate( 'Y-m-d-His' ) . '.csv', $csv );
        AjaxResponseBuilder::make()->success( __( 'Logs exported.', 'ultraguard-security-suite' ) )->addData( 'download_url', $url )->addNotification( __( 'Export ready.', 'ultraguard-security-suite' ), 'success' )->download( $url )->send();
    }

    public function handle_filter_attempts(): void {
        $this->handle_get_attempts();
    }

    public function handle_block_attempt_ip(): void {
        $this->verify();
        $ip = sanitize_text_field( wp_unslash( $_POST['ip_address'] ?? '' ) );
        if ( ! filter_var( $ip, FILTER_VALIDATE_IP ) ) {
            AjaxResponseBuilder::make()->error( __( 'Invalid IP address.', 'ultraguard-security-suite' ) )->addNotification( __( 'Invalid IP address.', 'ultraguard-security-suite' ), 'error' )->send();
            return;
        }
        $t  = $this->t( 'll_blocked_ips' );
        $id = $t ? $t->block_ip( $ip, 'blocked_from_attempt_log', 'manual', 'medium' ) : false;
        if ( $id ) {
            AjaxResponseBuilder::make()
                /* translators: %s: string */
                ->success( sprintf( __( 'IP %s blocked.', 'ultraguard-security-suite' ), $ip ) )
                /* translators: %s: string */
                ->addNotification( sprintf( __( 'IP %s blocked.', 'ultraguard-security-suite' ), $ip ), 'success' )
                ->send();
        } else {
            AjaxResponseBuilder::make()
                ->error( __( 'Failed to block IP.', 'ultraguard-security-suite' ) )
                ->addNotification( __( 'Failed to block IP.', 'ultraguard-security-suite' ), 'error' )
                ->send();
        }
    }

    public function handle_allow_attempt_ip(): void {
        $this->verify();
        $ip = sanitize_text_field( wp_unslash( $_POST['ip_address'] ?? '' ) );
        if ( ! filter_var( $ip, FILTER_VALIDATE_IP ) ) {
            AjaxResponseBuilder::make()->error( __( 'Invalid IP address.', 'ultraguard-security-suite' ) )->addNotification( __( 'Invalid IP address.', 'ultraguard-security-suite' ), 'error' )->send();
            return;
        }

        // Also release any lockout
        if ( $lo = $this->t( 'login_limiter_lockouts' ) ) { $lo->release( $ip ); }

        $t  = $this->t( 'll_allowed_ips' );
        $id = $t ? $t->allow_ip( $ip, 'allowed_from_attempt_log' ) : false;
        AjaxResponseBuilder::make()
            /* translators: %s: string */
            ->success( sprintf( __( 'IP %s added to allow list.', 'ultraguard-security-suite' ), $ip ) )
            /* translators: %s: string */
            ->addNotification( sprintf( __( 'IP %s added to allow list.', 'ultraguard-security-suite' ), $ip ), 'success' )
            ->send();
    }

    // ── IP Management ──────────────────────────────────────────────────────────

    public function handle_block_ip(): void {
        $this->verify();
        $ip       = sanitize_text_field( wp_unslash( $_POST['ip_address'] ?? '' ) );
        $reason   = sanitize_text_field( wp_unslash( $_POST['reason']      ?? 'manual' ) );
        $severity = sanitize_key( wp_unslash( $_POST['severity']           ?? 'medium'  ) );
        $expires  = sanitize_text_field( wp_unslash( $_POST['expires_at']  ?? '' ) ) ?: null;

        if ( ! filter_var( $ip, FILTER_VALIDATE_IP ) ) {
            AjaxResponseBuilder::make()->error( __( 'Invalid IP address.', 'ultraguard-security-suite' ) )->addNotification( __( 'Invalid IP address.', 'ultraguard-security-suite' ), 'error' )->send(); return;
        }

        $t  = $this->t( 'll_blocked_ips' );
        $id = $t ? $t->block_ip( $ip, $reason, 'manual', $severity, $expires ) : false;

        if ( $id ) {
            do_action( 'ultraguard_ip_list_updated', 'blocked', $ip );
            AjaxResponseBuilder::make()
                /* translators: %s: string */
                ->success( sprintf( __( 'IP %s blocked.', 'ultraguard-security-suite' ), $ip ) )
                ->addData( 'id', $id )
                ->addNotification( __( 'IP blocked!', 'ultraguard-security-suite' ), 'success' )
                ->send();
        } else {
            AjaxResponseBuilder::make()->error( __( 'Failed to block IP (may already be blocked).', 'ultraguard-security-suite' ) )->addNotification( __( 'IP may already be blocked.', 'ultraguard-security-suite' ), 'error' )->send();
        }
    }

    public function handle_allow_ip(): void {
        $this->verify();
        $ip    = sanitize_text_field( wp_unslash( $_POST['ip_address'] ?? '' ) );
        $label = sanitize_text_field( wp_unslash( $_POST['label']      ?? '' ) );

        if ( ! filter_var( $ip, FILTER_VALIDATE_IP ) ) {
            AjaxResponseBuilder::make()->error( __( 'Invalid IP address.', 'ultraguard-security-suite' ) )->addNotification( __( 'Invalid IP address.', 'ultraguard-security-suite' ), 'error' )->send(); return;
        }

        $t  = $this->t( 'll_allowed_ips' );
        $id = $t ? $t->allow_ip( $ip, $label ) : false;

        if ( $id ) {
            // Also release any active lockout for this IP
            if ( $lo = $this->t( 'login_limiter_lockouts' ) ) { $lo->release( $ip ); }

            do_action( 'ultraguard_ip_list_updated', 'allowed', $ip );
            AjaxResponseBuilder::make()
                /* translators: %s: string */
                ->success( sprintf( __( 'IP %s added to allow list.', 'ultraguard-security-suite' ), $ip ) )
                ->addData( 'id', $id )
                /* translators: %s: string */
                ->addNotification( sprintf( __( 'IP %s added to allow list.', 'ultraguard-security-suite' ), $ip ), 'success' )
                ->send();
        } else {
            AjaxResponseBuilder::make()->error( __( 'IP already in allow list or invalid.', 'ultraguard-security-suite' ) )->addNotification( __( 'IP already in allow list or invalid.', 'ultraguard-security-suite' ), 'error' )->send();
        }
    }

    public function handle_remove_blocked_ip(): void {
        $this->verify();
        $id = absint( $_POST['ip_id'] ?? 0 );
        $t  = $this->t( 'll_blocked_ips' );
        if ( $t && $t->unblock( $id ) ) {
            AjaxResponseBuilder::make()
                ->success( __( 'IP removed from block list.', 'ultraguard-security-suite' ) )
                ->addNotification( __( 'IP removed from block list.', 'ultraguard-security-suite' ), 'info' )
                ->addRemove( '#ug-ll-blocked-ip-row-' . $id, true, 300 )
                ->send();
        } else {
            AjaxResponseBuilder::make()->error( __( 'Failed to remove IP.', 'ultraguard-security-suite' ) )->addNotification( __( 'Failed to remove IP.', 'ultraguard-security-suite' ), 'error' )->send();
        }
    }

    public function handle_remove_allowed_ip(): void {
        $this->verify();
        $id = absint( $_POST['ip_id'] ?? 0 );
        $t  = $this->t( 'll_allowed_ips' );
        if ( $t && $t->remove( $id ) ) {
            AjaxResponseBuilder::make()
                ->success( __( 'IP removed from allow list.', 'ultraguard-security-suite' ) )
                ->addNotification( __( 'IP removed from allow list.', 'ultraguard-security-suite' ), 'info' )
                ->addRemove( '#ug-ll-allowed-ip-row-' . $id, true, 300 )
                ->send();
        } else {
            AjaxResponseBuilder::make()->error( __( 'Failed to remove IP.', 'ultraguard-security-suite' ) )->addNotification( __( 'Failed to remove IP.', 'ultraguard-security-suite' ), 'error' )->send();
        }
    }

    public function handle_bulk_block_ips(): void {
        $this->verify();
        $ips   = array_filter( array_map( 'sanitize_text_field', wp_unslash( (array) ( ( $_POST['ip_addresses'] ?? [] ) ) ) ), fn( $ip ) => filter_var( $ip, FILTER_VALIDATE_IP ) );
        $t     = $this->t( 'll_blocked_ips' );
        $count = 0;
        foreach ( $ips as $ip ) {
            if ( $t && $t->block_ip( $ip, 'bulk_block', 'bulk', 'medium' ) ) { $count++; }
        }
        do_action( 'ultraguard_ip_list_updated', 'blocked', null );
        /* translators: %d: number */
        $msg = sprintf( _n( '%d IP blocked.', '%d IPs blocked.', $count, 'ultraguard-security-suite' ), $count );
        AjaxResponseBuilder::make()
            ->success( $msg )
            ->addData( 'blocked', $count )
            ->addNotification( $msg, 'success' )
            ->addReload( 800 )
            ->send();
    }

    public function handle_bulk_allow_ips(): void {
        $this->verify();
        $ips   = array_filter( array_map( 'sanitize_text_field', wp_unslash( (array) ( ( $_POST['ip_addresses'] ?? [] ) ) ) ), fn( $ip ) => filter_var( $ip, FILTER_VALIDATE_IP ) );
        $t     = $this->t( 'll_allowed_ips' );
        $count = 0;
        foreach ( $ips as $ip ) {
            if ( $t && $t->allow_ip( $ip, 'bulk_allow' ) ) { $count++; }
        }
        /* translators: %d: number */
        $msg = sprintf( _n( '%d IP allowed.', '%d IPs allowed.', $count, 'ultraguard-security-suite' ), $count );
        AjaxResponseBuilder::make()
            ->success( $msg )
            ->addData( 'allowed', $count )
            ->addNotification( $msg, 'success' )
            ->addReload( 800 )
            ->send();
    }

    public function handle_bulk_remove_blocked(): void {
        $this->verify();
        $ids   = array_map( 'absint', (array) wp_unslash( $_POST['ip_ids'] ?? [] ) );
        $t     = $this->t( 'll_blocked_ips' );
        $count = 0;
        if ( $t ) { foreach ( $ids as $id ) { if ( $t->unblock( $id ) ) { $count++; } } }
        /* translators: %d: number */
        $msg = sprintf( _n( '%d IP removed.', '%d IPs removed.', $count, 'ultraguard-security-suite' ), $count );
        AjaxResponseBuilder::make()
            ->success( $msg )
            ->addData( 'removed', $count )
            ->addNotification( $msg, 'info' )
            ->addReload( 800 )
            ->send();
    }

    public function handle_import_ips(): void {
        $this->verify();
        $raw  = sanitize_textarea_field( wp_unslash( $_POST['ip_list'] ?? '' ) );
        $list = sanitize_key( wp_unslash( $_POST['list'] ?? 'blocked'  ) );
        $ips  = array_filter( array_map( 'trim', explode( "\n", $raw ) ), fn( $ip ) => filter_var( $ip, FILTER_VALIDATE_IP ) );

        $t = $list === 'allowed' ? $this->t( 'll_allowed_ips' ) : $this->t( 'll_blocked_ips' );
        $count = 0;
        foreach ( $ips as $ip ) {
            if ( $list === 'allowed' ) {
                if ( $t && $t->allow_ip( $ip, 'imported' ) ) { $count++; }
            } else {
                if ( $t && $t->block_ip( $ip, 'imported', 'bulk', 'medium' ) ) { $count++; }
            }
        }
        /* translators: %d: number */
        $msg = sprintf( _n( '%d IP imported.', '%d IPs imported.', $count, 'ultraguard-security-suite' ), $count );
        AjaxResponseBuilder::make()
            ->success( $msg )
            ->addData( 'imported', $count )
            ->addNotification( $msg, 'success' )
            ->addReload( 1000 )
            ->send();
    }

    public function handle_export_ips(): void {
        $this->verify();
        $list = sanitize_key( wp_unslash( $_POST['list'] ?? 'blocked'  ) );
        $t    = $list === 'allowed' ? $this->t( 'll_allowed_ips' ) : $this->t( 'll_blocked_ips' );
        $rows = $t ? $t->get_by( [], 100000, 'created_at', 'DESC' ) : [];
        $url  = $this->save_export( $list . '-ips-' . gmdate( 'Y-m-d-His' ) . '.json', wp_json_encode( $rows, JSON_PRETTY_PRINT ) );
        AjaxResponseBuilder::make()->success( __( 'IPs exported.', 'ultraguard-security-suite' ) )->addData( 'download_url', $url )->addData( 'count', count( $rows ) )->addNotification( __( 'IPs exported.', 'ultraguard-security-suite' ), 'success' )->download( $url )->send();
    }

    public function handle_validate_ips(): void {
        $this->verify();
        $raw    = sanitize_textarea_field( wp_unslash( $_POST['ip_list'] ?? '' ) );
        $lines  = array_map( 'trim', explode( "\n", $raw ) );
        $valid  = array_filter( $lines, fn( $ip ) => filter_var( $ip, FILTER_VALIDATE_IP ) );
        $invalid= array_diff( array_filter( $lines ), $valid );
        AjaxResponseBuilder::make()
            ->success()
            ->addData( 'valid',         array_values( $valid ) )
            ->addData( 'invalid',       array_values( $invalid ) )
            ->addData( 'valid_count',   count( $valid ) )
            ->addData( 'invalid_count', count( $invalid ) )
            /* translators: %1$d: integer, %2$d: integer */
            ->addNotification( sprintf( __( '%1$d valid, %2$d invalid IPs.', 'ultraguard-security-suite' ), count( $valid ), count( $invalid ) ), count( $invalid ) > 0 ? 'warning' : 'success' )
            ->send();
    }

    public function handle_view_ip_details(): void {
        $this->verify();
        $ip  = sanitize_text_field( wp_unslash( $_POST['ip_address'] ?? '' ) );
        $att = $this->t( 'login_attempts' );
        $lo  = $this->t( 'login_limiter_lockouts' );
        $bl  = $this->t( 'll_blocked_ips' );
        $wl  = $this->t( 'll_allowed_ips' );

        AjaxResponseBuilder::make()
            ->success()
            ->addData( 'ip',               $ip )
            ->addData( 'login_attempts',   $att ? $att->count( [ 'ip_address' => $ip ] ) : 0 )
            ->addData( 'failed_attempts',  $att ? $att->count( [ 'ip_address' => $ip, 'status' => 'failed' ] ) : 0 )
            ->addData( 'lockout_count',    $lo  ? $lo->count(  [ 'ip_address' => $ip ] ) : 0 )
            ->addData( 'currently_locked', $lo  ? $lo->is_locked_out( $ip ) : false )
            ->addData( 'is_blocked',       $bl  ? $bl->is_blocked( $ip ) : false )
            ->addData( 'is_allowed',       $wl  ? $wl->is_allowed( $ip ) : false )
            ->send();
    }

    public function handle_refresh_ip_stats(): void {
        $this->verify();
        $bl = $this->t( 'll_blocked_ips' );
        $wl = $this->t( 'll_allowed_ips' );
        $blocked = $bl ? $bl->count( [ 'is_active' => 1 ] ) : 0;
        $allowed = $wl ? $wl->count() : 0;
        AjaxResponseBuilder::make()
            ->success( __( 'IP stats refreshed.', 'ultraguard-security-suite' ) )
            ->addData( 'blocked', $blocked )
            ->addData( 'allowed', $allowed )
            ->updateText( '#ultraguard-ll-stat-blocked-ips', (string) $blocked )
            ->updateText( '#ultraguard-ll-stat-allowed-ips', (string) $allowed )
            ->send();
    }

    public function handle_clear_expired_ips(): void {
        $this->verify();
        $t = $this->t( 'll_blocked_ips' );
        if ( $t ) { $t->clear_expired(); }
        AjaxResponseBuilder::make()
            ->success( __( 'Expired IP blocks cleared.', 'ultraguard-security-suite' ) )
            ->addNotification( __( 'Expired IP blocks cleared.', 'ultraguard-security-suite' ), 'info' )
            ->addReload( 800 )
            ->send();
    }

    // ── Lockouts ───────────────────────────────────────────────────────────────

    public function handle_unlock_ip(): void {
        $this->verify();
        $ip = sanitize_text_field( wp_unslash( $_POST['ip_address'] ?? '' ) );
        if ( ! $ip ) { AjaxResponseBuilder::make()->error( __( 'IP address required.', 'ultraguard-security-suite' ) )->addNotification( __( 'IP address required.', 'ultraguard-security-suite' ), 'error' )->send(); return; }
        $lo = $this->t( 'login_limiter_lockouts' );
        if ( $lo && $lo->release( $ip ) ) {
            do_action( 'ultraguard_ip_unlocked', $ip );
            AjaxResponseBuilder::make()
                /* translators: %s: string */
                ->success( sprintf( __( 'IP %s unlocked.', 'ultraguard-security-suite' ), $ip ) )
                /* translators: %s: string */
                ->addNotification( sprintf( __( 'IP %s unlocked.', 'ultraguard-security-suite' ), $ip ), 'success' )
                ->addRemove( '.ultraguard-lockout-row[data-ip="' . esc_js( $ip ) . '"]', true, 300 )
                ->send();
        } else {
            AjaxResponseBuilder::make()->error( __( 'IP not found or already unlocked.', 'ultraguard-security-suite' ) )->addNotification( __( 'IP not found or already unlocked.', 'ultraguard-security-suite' ), 'error' )->send();
        }
    }

    public function handle_unlock_all(): void {
        $this->verify();
        $lo    = $this->t( 'login_limiter_lockouts' );
        $count = $lo ? $lo->release_all() : 0;
        do_action( 'ultraguard_all_lockouts_cleared' );
        AjaxResponseBuilder::make()
            ->success( __( 'All active lockouts cleared.', 'ultraguard-security-suite' ) )
            ->addData( 'cleared', $count )
            ->addNotification( __( 'All lockouts cleared!', 'ultraguard-security-suite' ), 'success' )
            ->emptyElement( '#ultraguard-ll-lockouts-table tbody' )
            ->updateText( '#ultraguard-ll-stat-active-lockouts', '0' )
            ->send();
    }

    public function handle_bulk_unlock(): void {
        $this->verify();
        $ips   = array_map( 'sanitize_text_field', wp_unslash( (array) ( ( $_POST['ip_addresses'] ?? [] ) ) ) );
        $lo    = $this->t( 'login_limiter_lockouts' );
        $count = 0;
        if ( $lo ) { foreach ( $ips as $ip ) { if ( $lo->release( $ip ) ) { $count++; } } }
        /* translators: %d: number */
        $msg = sprintf( _n( '%d IP unlocked.', '%d IPs unlocked.', $count, 'ultraguard-security-suite' ), $count );
        AjaxResponseBuilder::make()
            ->success( $msg )
            ->addData( 'unlocked', $count )
            ->addNotification( $msg, 'success' )
            ->addReload( 800 )
            ->send();
    }

    public function handle_bulk_blacklist(): void {
        $this->verify();
        $ips   = array_filter( array_map( 'sanitize_text_field', wp_unslash( (array) ( ( $_POST['ip_addresses'] ?? [] ) ) ) ), fn( $ip ) => filter_var( $ip, FILTER_VALIDATE_IP ) );
        $t     = $this->t( 'll_blocked_ips' );
        $count = 0;
        foreach ( $ips as $ip ) {
            if ( $t && $t->block_ip( $ip, 'bulk_blacklist_from_lockouts', 'bulk', 'high' ) ) { $count++; }
        }
        /* translators: %d: number */
        $msg = sprintf( _n( '%d IP blocked.', '%d IPs blocked.', $count, 'ultraguard-security-suite' ), $count );
        AjaxResponseBuilder::make()
            ->success( $msg )
            ->addData( 'blocked', $count )
            ->addNotification( $msg, 'success' )
            ->addReload( 800 )
            ->send();
    }

    public function handle_bulk_allowlist(): void {
        $this->verify();
        $ips   = array_filter( array_map( 'sanitize_text_field', wp_unslash( (array) ( ( $_POST['ip_addresses'] ?? [] ) ) ) ), fn( $ip ) => filter_var( $ip, FILTER_VALIDATE_IP ) );
        $t     = $this->t( 'll_allowed_ips' );
        $lo    = $this->t( 'login_limiter_lockouts' );
        $count = 0;
        foreach ( $ips as $ip ) {
            if ( $lo ) { $lo->release( $ip ); }
            if ( $t && $t->allow_ip( $ip, 'bulk_allowlist_from_lockouts' ) ) { $count++; }
        }
        /* translators: %d: number */
        $msg = sprintf( _n( '%d IP allowed.', '%d IPs allowed.', $count, 'ultraguard-security-suite' ), $count );
        AjaxResponseBuilder::make()
            ->success( $msg )
            ->addData( 'allowed', $count )
            ->addNotification( $msg, 'success' )
            ->addReload( 800 )
            ->send();
    }

    public function handle_export_lockouts(): void {
        $this->verify();
        $lo   = $this->t( 'login_limiter_lockouts' );
        $rows = $lo ? $lo->get_by( [], 100000, 'locked_at', 'DESC' ) : [];
        $url  = $this->save_export( 'lockouts-' . gmdate( 'Y-m-d-His' ) . '.json', wp_json_encode( $rows, JSON_PRETTY_PRINT ) );
        AjaxResponseBuilder::make()->success( __( 'Lockouts exported.', 'ultraguard-security-suite' ) )->addData( 'download_url', $url )->addData( 'count', count( $rows ) )->addNotification( __( 'Lockouts exported.', 'ultraguard-security-suite' ), 'success' )->download( $url )->send();
    }

    public function handle_clear_expired_lockouts(): void {
        $this->verify();
        $lo    = $this->t( 'login_limiter_lockouts' );
        $count = $lo ? $lo->cleanup_expired() : 0;
        AjaxResponseBuilder::make()
            ->success( __( 'Expired lockouts cleared.', 'ultraguard-security-suite' ) )
            ->addData( 'cleared', $count )
            ->addNotification( __( 'Expired lockouts cleared.', 'ultraguard-security-suite' ), 'info' )
            ->addReload( 800 )
            ->send();
    }

    public function handle_refresh_lockouts(): void {
        $this->verify();
        $lo = $this->t( 'login_limiter_lockouts' );
        $active_lockouts = $lo ? $lo->count( [ 'is_active' => 1 ] ) : 0;
        $total_lockouts  = $lo ? $lo->count() : 0;
        AjaxResponseBuilder::make()
            ->success( __( 'Lockouts refreshed.', 'ultraguard-security-suite' ) )
            ->addData( 'active_lockouts', $active_lockouts )
            ->addData( 'total_lockouts',  $total_lockouts )
            ->updateText( '#ultraguard-ll-stat-active-lockouts', (string) $active_lockouts )
            ->send();
    }

    // ── Row-level record mutations (table action buttons) ──────────────────────

    /**
     * Delete a single login attempt log entry by ID.
     *
     * POST: attempt_id (int)
     */
    public function handle_delete_attempt(): void {
        $this->verify();
        $id = absint( $_POST['attempt_id'] ?? 0 );
        if ( ! $id ) {
            AjaxResponseBuilder::make()->error( __( 'Invalid attempt ID.', 'ultraguard-security-suite' ) )->addNotification( __( 'Invalid attempt ID.', 'ultraguard-security-suite' ), 'error' )->send();
            return;
        }
        $t = $this->t( 'login_attempts' );
        if ( $t && $t->delete( $id ) ) {
            AjaxResponseBuilder::make()
                ->success( __( 'Log entry deleted.', 'ultraguard-security-suite' ) )
                ->addNotification( __( 'Log entry deleted.', 'ultraguard-security-suite' ), 'info' )
                ->addRemove( '#ug-ll-attempt-row-' . $id, true, 300 )
                ->send();
        } else {
            AjaxResponseBuilder::make()->error( __( 'Failed to delete log entry.', 'ultraguard-security-suite' ) )->addNotification( __( 'Failed to delete log entry.', 'ultraguard-security-suite' ), 'error' )->send();
        }
    }

    /**
     * Update a blocked-IP record's editable fields.
     *
     * POST: ip_id (int), reason (string), threat_level (string), expires_at (string|empty)
     */
    public function handle_edit_blocked_ip(): void {
        $this->verify();

        $id           = absint( $_POST['ip_id']       ?? 0 );
        $reason       = sanitize_text_field( wp_unslash( $_POST['reason']       ?? '' ) );
        $threat_level = sanitize_key( wp_unslash( $_POST['threat_level'] ?? 'medium'  ) );
        $expires_raw  = sanitize_text_field( wp_unslash( $_POST['expires_at']  ?? '' ) );
        $expires_at   = $expires_raw !== '' ? $expires_raw : null;

        if ( ! $id ) {
            AjaxResponseBuilder::make()->error( __( 'Invalid record ID.', 'ultraguard-security-suite' ) )->addNotification( __( 'Invalid record ID.', 'ultraguard-security-suite' ), 'error' )->send();
            return;
        }

        $valid_threats = [ 'low', 'medium', 'high', 'critical' ];
        if ( ! in_array( $threat_level, $valid_threats, true ) ) {
            $threat_level = 'medium';
        }

        // Validate date format when provided.
        if ( $expires_at !== null ) {
            $dt = \DateTimeImmutable::createFromFormat( 'Y-m-d H:i:s', $expires_at, new \DateTimeZone( 'UTC' ) );
            if ( ! $dt ) {
                AjaxResponseBuilder::make()->error( __( 'Invalid date format — use YYYY-MM-DD HH:MM:SS.', 'ultraguard-security-suite' ) )->addNotification( __( 'Invalid date format.', 'ultraguard-security-suite' ), 'error' )->send();
                return;
            }
        }

        $t = $this->t( 'll_blocked_ips' );
        if ( $t && $t->update( $id, [
            'reason'       => $reason,
            'threat_level' => $threat_level,
            'expires_at'   => $expires_at,
        ] ) ) {
            AjaxResponseBuilder::make()
                ->success( __( 'Blocked IP record updated.', 'ultraguard-security-suite' ) )
                ->addNotification( __( 'Saved!', 'ultraguard-security-suite' ), 'success' )
                ->closeModal( 'ultraguard-ll-edit-blocked-modal' )
                ->updateText( '#ug-ll-blocked-ip-row-' . $id . ' .ultraguard-cell-reason',       esc_html( $reason ) )
                ->updateText( '#ug-ll-blocked-ip-row-' . $id . ' .ultraguard-cell-threat-level', esc_html( $threat_level ) )
                ->updateText( '#ug-ll-blocked-ip-row-' . $id . ' .ultraguard-cell-expires',      $expires_at ? esc_html( $expires_at ) : __( 'Never', 'ultraguard-security-suite' ) )
                ->send();
        } else {
            AjaxResponseBuilder::make()->error( __( 'Failed to update record.', 'ultraguard-security-suite' ) )->addNotification( __( 'Failed to update record.', 'ultraguard-security-suite' ), 'error' )->send();
        }
    }

    /**
     * Update an allowed-IP record's label.
     *
     * POST: ip_id (int), label (string)
     */
    public function handle_edit_allowed_ip(): void {
        $this->verify();

        $id    = absint( $_POST['ip_id'] ?? 0 );
        $label = sanitize_text_field( wp_unslash( $_POST['label'] ?? '' ) );

        if ( ! $id ) {
            AjaxResponseBuilder::make()->error( __( 'Invalid record ID.', 'ultraguard-security-suite' ) )->addNotification( __( 'Invalid record ID.', 'ultraguard-security-suite' ), 'error' )->send();
            return;
        }

        $t = $this->t( 'll_allowed_ips' );
        if ( $t && $t->update( $id, [ 'label' => $label ] ) ) {
            AjaxResponseBuilder::make()
                ->success( __( 'Allowed IP label updated.', 'ultraguard-security-suite' ) )
                ->addNotification( __( 'Saved!', 'ultraguard-security-suite' ), 'success' )
                ->closeModal( 'ultraguard-ll-edit-allowed-modal' )
                ->updateText( '#ug-ll-allowed-ip-row-' . $id . ' .ultraguard-cell-label', esc_html( $label ) )
                ->send();
        } else {
            AjaxResponseBuilder::make()->error( __( 'Failed to update label.', 'ultraguard-security-suite' ) )->addNotification( __( 'Failed to update label.', 'ultraguard-security-suite' ), 'error' )->send();
        }
    }

    /**
     * Hard-delete a single lockout record by ID.
     *
     * POST: lockout_id (int)
     */
    public function handle_delete_lockout(): void {
        $this->verify();

        $id = absint( $_POST['lockout_id'] ?? 0 );
        if ( ! $id ) {
            AjaxResponseBuilder::make()->error( __( 'Invalid lockout ID.', 'ultraguard-security-suite' ) )->addNotification( __( 'Invalid lockout ID.', 'ultraguard-security-suite' ), 'error' )->send();
            return;
        }

        $t = $this->t( 'login_limiter_lockouts' );
        if ( $t && $t->delete( $id ) ) {
            AjaxResponseBuilder::make()
                ->success( __( 'Lockout record deleted.', 'ultraguard-security-suite' ) )
                ->addNotification( __( 'Lockout record deleted.', 'ultraguard-security-suite' ), 'info' )
                ->addRemove( '#ug-ll-lockout-row-' . $id, true, 300 )
                ->send();
        } else {
            AjaxResponseBuilder::make()->error( __( 'Failed to delete lockout record.', 'ultraguard-security-suite' ) )->addNotification( __( 'Failed to delete lockout record.', 'ultraguard-security-suite' ), 'error' )->send();
        }
    }

    // ── Sessions ───────────────────────────────────────────────────────────────

    public function handle_get_sessions(): void {
        $this->verify();
        $user_id = absint( $_POST['user_id'] ?? 0 );
        $t       = $this->t( 'user_sessions' );
        $data    = $user_id
            ? ( $t ? $t->get_active_for_user( $user_id ) : [] )
            : ( $t ? $t->get_by( [ 'is_active' => 1 ], 200, 'last_active', 'DESC' ) : [] );
        AjaxResponseBuilder::make()->success()->addData( 'records', $data )->send();
    }

    public function handle_revoke_session(): void {
        $this->verify();
        $id = absint( $_POST['session_id'] ?? 0 );
        $t  = $this->t( 'user_sessions' );
        if ( $t && $t->revoke( $id ) ) {
            AjaxResponseBuilder::make()
                ->success( __( 'Session revoked.', 'ultraguard-security-suite' ) )
                ->addNotification( __( 'Session revoked.', 'ultraguard-security-suite' ), 'info' )
                ->addRemove( '#ug-ll-session-row-' . $id, true, 300 )
                ->send();
        } else {
            AjaxResponseBuilder::make()->error( __( 'Failed to revoke session.', 'ultraguard-security-suite' ) )->addNotification( __( 'Failed to revoke session.', 'ultraguard-security-suite' ), 'error' )->send();
        }
    }

    public function handle_revoke_user_sessions(): void {
        $this->verify();
        $user_id = absint( $_POST['user_id'] ?? 0 );
        $t       = $this->t( 'user_sessions' );
        if ( $t && $user_id ) { $t->revoke_all_for_user( $user_id ); }
        /* translators: %d: integer */
        AjaxResponseBuilder::make()->success( sprintf( __( 'All sessions revoked for user #%d.', 'ultraguard-security-suite' ), $user_id ) )->addNotification( sprintf( __( 'All sessions revoked for user #%d.', 'ultraguard-security-suite' ), $user_id ), 'success' )->emptyElement( '#ultraguard-ll-sessions-table tbody[data-user-id="' . $user_id . '"]' )->send();
    }

    // ── Settings ───────────────────────────────────────────────────────────────

    private function field_schema(): array {
        return [
            'loginlimiter_enabled'                => [ 'type' => 'bool' ],
            'loginlimiter_progressive_lockout'    => [ 'type' => 'bool' ],
            'loginlimiter_notify_admin'           => [ 'type' => 'bool' ],
            'loginlimiter_hide_errors'            => [ 'type' => 'bool' ],
            'loginlimiter_two_factor_enabled'     => [ 'type' => 'bool' ],
            'loginlimiter_2fa_required_admin'     => [ 'type' => 'bool' ],
            'loginlimiter_single_session'         => [ 'type' => 'bool' ],
            'loginlimiter_track_activity'         => [ 'type' => 'bool' ],
            'loginlimiter_captcha_enabled'        => [ 'type' => 'bool' ],
            'loginlimiter_clear_lockout_on_success' => [ 'type' => 'bool' ],
            'loginlimiter_max_attempts'     => [ 'type' => 'int', 'min' => 1,  'max' => 20,    'default' => 5  ],
            'loginlimiter_lockout_duration' => [ 'type' => 'int', 'min' => 1,  'max' => 1440,  'default' => 30 ],
            'loginlimiter_attempt_window'   => [ 'type' => 'int', 'min' => 1,  'max' => 1440,  'default' => 10 ],
            'loginlimiter_2fa_code_expiry'  => [ 'type' => 'int', 'min' => 1,  'max' => 30,    'default' => 10 ],
            'loginlimiter_session_timeout'  => [ 'type' => 'int', 'min' => 0,  'max' => 43200, 'default' => 0  ],
            'loginlimiter_captcha_after'    => [ 'type' => 'int', 'min' => 0,  'max' => 10,    'default' => 3  ],
            'loginlimiter_log_retention'    => [ 'type' => 'int', 'min' => 1,  'max' => 365,   'default' => 30 ],
            'loginlimiter_captcha_provider'       => [ 'type' => 'select', 'default' => 'turnstile', 'options' => [ 'turnstile', 'recaptcha_v2', 'recaptcha_v3' ] ],
            'loginlimiter_turnstile_site_key'     => [ 'type' => 'text', 'default' => '' ],
            'loginlimiter_turnstile_secret_key'   => [ 'type' => 'text', 'default' => '' ],
            'loginlimiter_recaptcha_site_key'     => [ 'type' => 'text', 'default' => '' ],
            'loginlimiter_recaptcha_secret_key'   => [ 'type' => 'text', 'default' => '' ],
            'loginlimiter_recaptcha_v3_min_score' => [ 'type' => 'float', 'default' => 0.5, 'min' => 0.1, 'max' => 1.0 ],
            'loginlimiter_2fa_method'          => [ 'type' => 'select', 'options' => [ 'email', 'totp', 'sms' ], 'default' => 'email' ],
            'loginlimiter_custom_login_slug'   => [ 'type' => 'key', 'default' => '' ],
            'login_protect_enabled'              => [ 'type' => 'bool' ],
            'login_protect_log_attempts'         => [ 'type' => 'bool' ],
            'login_protect_auto_block'           => [ 'type' => 'bool' ],
            'login_protect_notify_admin'         => [ 'type' => 'bool' ],
            'login_protect_custom_path'          => [ 'type' => 'key', 'default' => '' ],
            'login_protect_original_action'      => [ 'type' => 'select', 'options' => [ '404', 'redirect_home', 'redirect_custom' ], 'default' => '404' ],
            'login_protect_custom_redirect_url'  => [ 'type' => 'text', 'default' => '' ],
            'login_protect_auto_block_threshold' => [ 'type' => 'int', 'min' => 1, 'max' => 100, 'default' => 5 ],
        ];
    }

    public function handle_save_settings(): void {
        $this->verify();

        $data = $this->sanitize_fields( (array) wp_unslash( $_POST ), $this->field_schema() );

        $login_limiter_data = array_filter(
            $data,
            static fn( string $key ): bool => strpos( $key, 'loginlimiter_' ) === 0,
            ARRAY_FILTER_USE_KEY
        );

        $login_protect_data = array_filter(
            $data,
            static fn( string $key ): bool => strpos( $key, 'login_protect_' ) === 0,
            ARRAY_FILTER_USE_KEY
        );

        $ok_login_limiter = true;
        if ( ! empty( $login_limiter_data ) ) {
            $existing_login_limiter = $this->plugin->settings->get_module_settings( 'loginlimiter' );
            $merged_login_limiter   = array_merge( $existing_login_limiter, $login_limiter_data );

            $ok_login_limiter = ( $merged_login_limiter === $existing_login_limiter )
                || $this->plugin->settings->save_module_settings( 'loginlimiter', $login_limiter_data );
        }

        $ok_login_protect = true;
        if ( ! empty( $login_protect_data ) ) {
            $existing_login_protect = $this->plugin->settings->get_module_settings( 'login_protect' );
            $merged_login_protect   = array_merge( $existing_login_protect, $login_protect_data );

            $ok_login_protect = ( $merged_login_protect === $existing_login_protect )
                || $this->plugin->settings->save_module_settings( 'login_protect', $login_protect_data );
        }

        $ok = $ok_login_limiter && $ok_login_protect;

        if ( ! $ok ) {
            AjaxResponseBuilder::make()->error( __( 'Failed to save Login Limiter settings.', 'ultraguard-security-suite' ) )->addNotification( __( 'Failed to save Login Limiter settings.', 'ultraguard-security-suite' ), 'error' )->send();
            return;
        }

        do_action( 'ultraguard_login_limiter_settings_saved' );

        AjaxResponseBuilder::make()
            ->success( __( 'Login Limiter settings saved.', 'ultraguard-security-suite' ) )
            ->addNotification( __( 'Settings saved!', 'ultraguard-security-suite' ), 'success' )
            ->send();
    }

    public function handle_test_protection(): void {
        $this->verify();
        $result = $this->engine()->run_protection_test();
        /* translators: %1$s: string, %2$d: integer */
        $msg = sprintf( __( 'Protection test complete — grade: %1$s (%2$d/100)', 'ultraguard-security-suite' ), $result['grade'], $result['score'] );
        $score = (int) ( $result['score'] ?? 0 );
        AjaxResponseBuilder::make()
            ->success( $msg )
            ->mergeData( $result )
            ->addNotification( $msg, $score >= 75 ? 'success' : ( $score >= 50 ? 'warning' : 'error' ) )
            ->updateText( '#ultraguard-ll-protection-grade', esc_html( $result['grade'] ?? '' ) )
            ->updateText( '#ultraguard-ll-protection-score', (string) $score )
            ->send();
    }

    public function handle_security_audit(): void {
        $this->verify();
        $result = $this->engine()->run_protection_test();
        $att    = $this->t( 'login_attempts' );
        $result['brute_force_24h'] = $att ? $att->count( [ 'status' => 'failed', 'created_at >=' => gmdate( 'Y-m-d H:i:s', strtotime( '-24 hours' ) ) ] ) : 0;
        /* translators: %d: integer */
        $audit_msg = sprintf( __( 'Security audit complete — score: %d/100', 'ultraguard-security-suite' ), $result['score'] );
        AjaxResponseBuilder::make()
            ->success( $audit_msg )
            ->mergeData( $result )
            ->addNotification( $audit_msg, (int) $result['score'] >= 75 ? 'success' : 'warning' )
            ->send();
    }

    public function handle_export_audit(): void {
        $this->verify();
        $att    = $this->t( 'login_attempts' );
        $lo     = $this->t( 'login_limiter_lockouts' );
        $result = $this->engine()->run_protection_test();

        $report = [
            'generated'       => gmdate( 'Y-m-d H:i:s' ),
            'site_url'        => get_bloginfo( 'url' ),
            'total_attempts'  => $att ? $att->count() : 0,
            'failed_attempts' => $att ? $att->count( [ 'status' => 'failed' ] ) : 0,
            'total_lockouts'  => $lo  ? $lo->count() : 0,
            'protection_test' => $result,
        ];

        $url = $this->save_export( 'login-audit-' . gmdate( 'Y-m-d-His' ) . '.json', wp_json_encode( $report, JSON_PRETTY_PRINT ) );
        AjaxResponseBuilder::make()->success( __( 'Audit report exported.', 'ultraguard-security-suite' ) )->addData( 'download_url', $url )->addNotification( __( 'Audit report ready.', 'ultraguard-security-suite' ), 'success' )->download( $url )->send();
    }

    public function handle_reset_stats(): void {
        $this->verify();
        $t = $this->t( 'login_attempts' );
        if ( $t ) { $t->truncate(); }
        AjaxResponseBuilder::make()
            ->success( __( 'Login statistics reset.', 'ultraguard-security-suite' ) )
            ->addNotification( __( 'Stats reset!', 'ultraguard-security-suite' ), 'success' )
            ->emptyElement( '#ultraguard-ll-attempts-table tbody' )
            ->updateText( '#ultraguard-ll-stat-failed-today',  '0' )
            ->updateText( '#ultraguard-ll-stat-success-today', '0' )
            ->send();
    }

    // ── Private helpers ────────────────────────────────────────────────────────

    /**
     * Verify nonce and capability. Sends JSON error + dies on failure.
     */
    private function verify(): void {
        if ( ! check_ajax_referer( self::NONCE, '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 );
        }
    }

    /**
     * Retrieve a DB table by key.
     *
     * @param  string $key
     * @return \UltraguardPro\db\BaseTable|null
     */
    private function t( string $key ) {
        $db = $this->plugin->modules->get_db_tables();
        return $db ? $db->get_table( $key ) : null;
    }

    /**
     * Lazily create the brute-force engine.
     *
     * @return LoginLimiterEngine
     */
    private function engine(): LoginLimiterEngine {
        static $engine = null;
        if ( null === $engine ) {
            $engine = new LoginLimiterEngine( $this->plugin );
        }
        return $engine;
    }

    /**
     * Save an export file and return its public URL.
     *
     * @param  string $filename
     * @param  string $content
     * @return string  Public URL.
     */
    private function save_export( string $filename, string $content ): string {
        $upload = wp_upload_dir();
        $dir    = trailingslashit( $upload['basedir'] ) . 'ultraguard-security-suite/exports/';
        wp_mkdir_p( $dir );

        if ( ! file_exists( $dir . 'index.php' ) ) {
            file_put_contents( $dir . 'index.php', '<?php // silence' );
        }

        file_put_contents( $dir . $filename, $content );

        return trailingslashit( $upload['baseurl'] ) . 'ultraguard-security-suite/exports/' . $filename;
    }
    // ── Realtime fallback handler ─────────────────────────────────────────────

    /**
     * RT fallback: login_status channel.
     * Reuses the same table queries as handle_refresh_stats() so data shapes match.
     */
    public function handle_rt_login_status(): void {
        if ( ! check_ajax_referer( 'ultraguard_admin_nonce', '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 );
            return;
        }

        $today   = gmdate( 'Y-m-d 00:00:00' );
        $att_tbl = $this->t( 'login_attempts' );
        $lo_tbl  = $this->t( 'login_limiter_lockouts' );
        $bl_tbl  = $this->t( 'll_blocked_ips' );

        $attempts_today = $att_tbl ? (int) $att_tbl->count( [ 'status' => 'failed', 'created_at >=' => $today ] ) : 0;
        $locked_now     = $lo_tbl  ? (int) $lo_tbl->count( [ 'is_active' => 1 ] ) : 0;
        $blocked_ips    = $bl_tbl  ? (int) $bl_tbl->count( [ 'is_active' => 1 ] ) : 0;

        $last_attempt_human = '';
        if ( $att_tbl && method_exists( $att_tbl, 'get_last_row' ) ) {
            $row = $att_tbl->get_last_row( [ 'status' => 'failed' ] );
            if ( ! empty( $row['created_at'] ) ) {
                $last_attempt_human = human_time_diff( strtotime( $row['created_at'] ), time() )
                    . ' ' . __( 'ago', 'ultraguard-security-suite' );
            }
        }

        $recent_attempts = [];
        if ( $att_tbl && method_exists( $att_tbl, 'get_recent' ) ) {
            $recent_attempts = array_slice( (array) $att_tbl->get_recent( 5 ), 0, 5 );
        }

        AjaxResponseBuilder::make()
            ->success()
            ->addData( 'attempts_today',     $attempts_today )
            ->addData( 'locked_now',         $locked_now )
            ->addData( 'blocked_ips',        $blocked_ips )
            ->addData( 'last_attempt_human', $last_attempt_human )
            ->addData( 'recent_attempts',    $recent_attempts )
            ->updateText( '#ultraguard-ll-rt-attempts-today', (string) $attempts_today )
            ->updateText( '#ultraguard-ll-rt-locked-now',     (string) $locked_now )
            ->updateText( '#ultraguard-ll-rt-blocked-ips',    (string) $blocked_ips )
            ->updateText( '#ultraguard-ll-rt-last-attempt',   $last_attempt_human )
            ->send();
    }

}

// phpcs:enable
