<?php

declare(strict_types=1);

namespace SecurWP\Modules\Firewall\API;

use SecurWP\Includes\IpDetector;
use SecurWP\Includes\IpProbe;
use SecurWP\Includes\IpUtils;
use SecurWP\Includes\ProxyPresets;
use SecurWP\Includes\SettingsStore;
use SecurWP\Modules\Cloudflare\Manager as CloudflareManager;
use SecurWP\Modules\Firewall\Blocklist\BlocklistService;
use SecurWP\Modules\Firewall\ExtendedProtection\Manager as ExtendedProtectionManager;
use SecurWP\Modules\Firewall\FirewallBypass;
use SecurWP\Modules\Firewall\FirewallConfigService;
use SecurWP\Modules\Firewall\FirewallLogger;
use SecurWP\Modules\Firewall\FirewallSettings;
use SecurWP\Modules\Firewall\GeoIP\GeoIPService;
use SecurWP\Modules\Firewall\GeoIP\MaxMindDownloader;
use SecurWP\Modules\Firewall\LockoutManager;
use SecurWP\Modules\Firewall\WHOIS\WHOISLookup;

use WP_REST_Server;
use WP_REST_Request;
use WP_REST_Response;

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

class FirewallApi {

    public function register_routes() {
        // --- Existing routes ---

        register_rest_route( 'securwp/v1', '/firewall/settings', [
            [
                'methods'             => WP_REST_Server::READABLE,
                'callback'            => [ $this, 'get_settings' ],
                'permission_callback' => [ $this, 'check_permission' ],
            ],
            [
                'methods'             => WP_REST_Server::CREATABLE,
                'callback'            => [ $this, 'update_settings' ],
                'permission_callback' => [ $this, 'check_permission' ],
            ],
        ] );

         register_rest_route( 'securwp/v1', '/firewall/ip', [
            [
                'methods'             => WP_REST_Server::READABLE,
                'callback'            => [ $this, 'get_detected_ip' ],
                'permission_callback' => [ $this, 'check_permission' ],
            ],
        ] );

        register_rest_route( 'securwp/v1', '/firewall/ip/diagnose', [
            [
                'methods'             => WP_REST_Server::READABLE,
                'callback'            => [ $this, 'diagnose_ip' ],
                'permission_callback' => [ $this, 'check_permission' ],
            ],
        ] );

        register_rest_route( 'securwp/v1', '/firewall/proxy-presets', [
            [
                'methods'             => WP_REST_Server::READABLE,
                'callback'            => [ $this, 'get_proxy_presets' ],
                'permission_callback' => [ $this, 'check_permission' ],
            ],
        ] );

        // --- GeoIP routes ---

        register_rest_route( 'securwp/v1', '/firewall/geoip/settings', [
            [
                'methods'             => WP_REST_Server::READABLE,
                'callback'            => [ $this, 'get_geoip_settings' ],
                'permission_callback' => [ $this, 'check_permission' ],
            ],
            [
                'methods'             => WP_REST_Server::CREATABLE,
                'callback'            => [ $this, 'update_geoip_settings' ],
                'permission_callback' => [ $this, 'check_permission' ],
            ],
        ] );

        register_rest_route( 'securwp/v1', '/firewall/geoip/update-db', [
            [
                'methods'             => WP_REST_Server::CREATABLE,
                'callback'            => [ $this, 'update_geoip_db' ],
                'permission_callback' => [ $this, 'check_permission' ],
            ],
        ] );

        register_rest_route( 'securwp/v1', '/firewall/geoip/lookup', [
            [
                'methods'             => WP_REST_Server::READABLE,
                'callback'            => [ $this, 'geoip_lookup' ],
                'permission_callback' => [ $this, 'check_permission' ],
                'args'                => [
                    'ip' => [
                        'required'          => true,
                        'type'              => 'string',
                        'validate_callback' => function( $param ) {
                            return filter_var( $param, FILTER_VALIDATE_IP ) !== false;
                        },
                        'sanitize_callback' => 'sanitize_text_field',
                    ],
                ],
            ],
        ] );

        // --- Blocklist routes ---

        register_rest_route( 'securwp/v1', '/firewall/blocklist/settings', [
            [
                'methods'             => WP_REST_Server::READABLE,
                'callback'            => [ $this, 'get_blocklist_settings' ],
                'permission_callback' => [ $this, 'check_permission' ],
            ],
            [
                'methods'             => WP_REST_Server::CREATABLE,
                'callback'            => [ $this, 'update_blocklist_settings' ],
                'permission_callback' => [ $this, 'check_permission' ],
            ],
        ] );

        register_rest_route( 'securwp/v1', '/firewall/blocklist/sync', [
            [
                'methods'             => WP_REST_Server::CREATABLE,
                'callback'            => [ $this, 'sync_blocklist' ],
                'permission_callback' => [ $this, 'check_permission' ],
            ],
        ] );

        register_rest_route( 'securwp/v1', '/firewall/blocklist/stats', [
            [
                'methods'             => WP_REST_Server::READABLE,
                'callback'            => [ $this, 'get_blocklist_stats' ],
                'permission_callback' => [ $this, 'check_permission' ],
            ],
        ] );

        register_rest_route( 'securwp/v1', '/firewall/extended-protection', [
            [
                'methods'             => WP_REST_Server::READABLE,
                'callback'            => [ $this, 'get_extended_protection_status' ],
                'permission_callback' => [ $this, 'check_permission' ],
            ],
            [
                'methods'             => WP_REST_Server::CREATABLE,
                'callback'            => [ $this, 'update_extended_protection' ],
                'permission_callback' => [ $this, 'check_permission' ],
            ],
        ] );

        // --- WHOIS lookup route ---

        register_rest_route( 'securwp/v1', '/firewall/whois', [
            [
                'methods'             => WP_REST_Server::READABLE,
                'callback'            => [ $this, 'whois_lookup' ],
                'permission_callback' => [ $this, 'check_permission' ],
                'args'                => [
                    'ip' => [
                        'required'          => true,
                        'type'              => 'string',
                        'validate_callback' => function( $param ) {
                            return filter_var( $param, FILTER_VALIDATE_IP ) !== false;
                        },
                        'sanitize_callback' => 'sanitize_text_field',
                    ],
                ],
            ],
        ] );

        // --- IP Lockout management routes ---

        register_rest_route( 'securwp/v1', '/firewall/lockouts', [
            [
                'methods'             => WP_REST_Server::READABLE,
                'callback'            => [ $this, 'get_lockouts' ],
                'permission_callback' => [ $this, 'check_permission' ],
                'args'                => [
                    'type' => [
                        'required' => false,
                        'type'     => 'string',
                        'enum'     => [ 'ban', 'allow' ],
                    ],
                ],
            ],
            [
                'methods'             => WP_REST_Server::CREATABLE,
                'callback'            => [ $this, 'add_lockout' ],
                'permission_callback' => [ $this, 'check_permission' ],
            ],
        ] );

        register_rest_route( 'securwp/v1', '/firewall/lockouts/remove', [
            [
                'methods'             => WP_REST_Server::CREATABLE,
                'callback'            => [ $this, 'remove_lockout' ],
                'permission_callback' => [ $this, 'check_permission' ],
            ],
        ] );

        register_rest_route( 'securwp/v1', '/firewall/lockouts/purge-expired', [
            [
                'methods'             => WP_REST_Server::CREATABLE,
                'callback'            => [ $this, 'purge_expired_lockouts' ],
                'permission_callback' => [ $this, 'check_permission' ],
            ],
        ] );

        // --- IP probe routes ---

        // Public endpoint: receives the IntelEngine callback. No auth required
        // because IntelEngine cannot authenticate as a WP admin. Security is
        // provided by the one-time token validated inside the handler.
        register_rest_route( 'securwp/v1', '/firewall/ip-probe', [
            [
                'methods'             => WP_REST_Server::READABLE,
                'callback'            => [ $this, 'handle_ip_probe_callback' ],
                'permission_callback' => '__return_true',
                'args'                => [
                    'token' => [
                        'required'          => true,
                        'type'              => 'string',
                        'sanitize_callback' => 'sanitize_text_field',
                    ],
                    'ip' => [
                        'required'          => true,
                        'type'              => 'string',
                        'sanitize_callback' => 'sanitize_text_field',
                    ],
                ],
            ],
        ] );

        register_rest_route( 'securwp/v1', '/firewall/ip-probe/status', [
            [
                'methods'             => WP_REST_Server::READABLE,
                'callback'            => [ $this, 'get_ip_probe_status' ],
                'permission_callback' => [ $this, 'check_permission' ],
            ],
        ] );

        register_rest_route( 'securwp/v1', '/firewall/ip-probe/trigger', [
            [
                'methods'             => WP_REST_Server::CREATABLE,
                'callback'            => [ $this, 'trigger_ip_probe' ],
                'permission_callback' => [ $this, 'check_permission' ],
            ],
        ] );

        // --- Firewall event log ---

        register_rest_route( 'securwp/v1', '/firewall/events', [
            [
                'methods'             => WP_REST_Server::READABLE,
                'callback'            => [ $this, 'get_events' ],
                'permission_callback' => [ $this, 'check_permission' ],
                'args'                => [
                    'per_page'    => [ 'type' => 'integer', 'default' => 50, 'minimum' => 1, 'maximum' => 200 ],
                    'page'        => [ 'type' => 'integer', 'default' => 1,  'minimum' => 1 ],
                    'attack_type' => [ 'type' => 'string',  'default' => '' ],
                    'ip'          => [ 'type' => 'string',  'default' => '' ],
                    'since'       => [ 'type' => 'integer', 'default' => 0 ],
                    'until'       => [ 'type' => 'integer', 'default' => 0 ],
                ],
            ],
        ] );
    }

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

    /**
     * GET /securwp/v1/firewall/events
     *
     * Returns paginated firewall block events with optional filters.
     * Response shape mirrors DashboardLogEntry so the feed can reuse types.
     *
     * @param WP_REST_Request $request
     * @return WP_REST_Response
     */
    public function get_events( WP_REST_Request $request ): WP_REST_Response {
        if ( ! FirewallLogger::is_schema_current() ) {
            return new WP_REST_Response( [
                'items' => [],
                'total' => 0,
                'pages' => 1,
            ] );
        }

        global $wpdb;
        $table = self::table_identifier( $wpdb->prefix . 'securwp_firewall_events' );

        $per_page    = (int) $request->get_param( 'per_page' );
        $page        = (int) $request->get_param( 'page' );
        $attack_type = sanitize_text_field( $request->get_param( 'attack_type' ) );
        $ip          = sanitize_text_field( $request->get_param( 'ip' ) );
        $since       = (int) $request->get_param( 'since' );
        $until       = (int) $request->get_param( 'until' );

        $where  = [ 'blog_id = %d' ];
        $params = [ get_current_blog_id() ];

        if ( $since > 0 ) {
            $where[]  = 'event_time >= %d';
            $params[] = $since;
        }
        if ( $until > 0 ) {
            $where[]  = 'event_time <= %d';
            $params[] = $until;
        }
        if ( $attack_type !== '' ) {
            $where[]  = 'attack_type LIKE %s';
            $params[] = '%' . $wpdb->esc_like( $attack_type ) . '%';
        }
        if ( $ip !== '' ) {
            $where[]  = 'ip_address LIKE %s';
            $params[] = $wpdb->esc_like( $ip ) . '%';
        }

        $where_sql = 'WHERE ' . implode( ' AND ', $where );
        $offset    = ( $page - 1 ) * $per_page;

        // Total count for pagination headers.
        // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare,PluginCheck.Security.DirectDB.UnescapedDBParameter
        $total = (int) $wpdb->get_var(
            $wpdb->prepare( "SELECT COUNT(*) FROM {$table} {$where_sql}", ...$params )
        );
        // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare,PluginCheck.Security.DirectDB.UnescapedDBParameter

        // Fetch page.
        $limit_params = array_merge( $params, [ $per_page, $offset ] );
        // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber,PluginCheck.Security.DirectDB.UnescapedDBParameter
        $rows = $wpdb->get_results(
            $wpdb->prepare(
                "SELECT id, event_time, ip_address, request_uri, http_method, http_status, attack_type, user_agent
                 FROM {$table} {$where_sql} ORDER BY event_time DESC LIMIT %d OFFSET %d",
                ...$limit_params
            ),
            ARRAY_A
        );
        // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber,PluginCheck.Security.DirectDB.UnescapedDBParameter

        $items = [];
        if ( is_array( $rows ) ) {
            foreach ( $rows as $row ) {
                $attack = (string) $row['attack_type'];
                $items[] = [
                    'id'          => (int) $row['id'],
                    'timestamp'   => gmdate( 'Y-m-d H:i:s', (int) $row['event_time'] ),
                    'event_time'  => (int) $row['event_time'],
                    'ip_address'  => (string) $row['ip_address'],
                    'request_uri' => (string) $row['request_uri'],
                    'http_method' => (string) $row['http_method'],
                    'http_status' => (int) $row['http_status'],
                    'attack_type' => $attack,
                    'severity'    => \SecurWP\Modules\Firewall\FirewallLogger::get_severity( $attack ),
                    'user_agent'  => (string) $row['user_agent'],
                ];
            }
        }

        $pages    = max( 1, (int) ceil( $total / $per_page ) );
        $response = new WP_REST_Response( [
            'items' => $items,
            'total' => $total,
            'pages' => $pages,
        ], 200 );

        $response->header( 'X-WP-Total',      (string) $total );
        $response->header( 'X-WP-TotalPages', (string) $pages );

        return $response;
    }

    /**
     * Escape a plugin-owned table identifier for direct interpolation.
     */
    private static function table_identifier( string $table ): string {
        if ( $table === '' || strlen( $table ) > 64 || ! preg_match( '/^[A-Za-z0-9_]+$/', $table ) ) {
            throw new \InvalidArgumentException( 'Invalid table identifier.' );
        }

        return '`' . str_replace( '`', '``', $table ) . '`';
    }

    // =========================================================================
    // Existing endpoints
    // =========================================================================

    public function get_settings() {
        $merged = $this->get_effective_firewall_settings();

        $redacted = FirewallSettings::redact_sensitive_keys( $merged );
        $waf_status = $this->get_extended_waf_status();
        $redacted['extended_waf_active'] = (bool) ( $waf_status['active'] ?? false );
        $redacted['extended_waf_server_type'] = sanitize_text_field( (string) ( $waf_status['server_type'] ?? '' ) );

        return new WP_REST_Response( $redacted );
    }

    public function update_settings( WP_REST_Request $request ) {
        $params = $request->get_json_params();
        if ( ! is_array( $params ) ) {
            return new WP_REST_Response( [ 'error' => 'Invalid JSON' ], 400 );
        }

        $current = FirewallSettings::get();
        $previous_settings = $current;
        $requested_allowlist = array_key_exists( 'allowlist', $params ) ? $params['allowlist'] : null;
        $patch_result = $this->apply_interactive_settings_patch( $current, $params, [ 'banned_ips', 'allowlist' ] );
        if ( $patch_result instanceof WP_REST_Response ) {
            return $patch_result;
        }
        $new_settings = $patch_result['settings'];

        if ( ! $this->save_interactive_firewall_settings(
            $new_settings,
            is_array( $patch_result['readiness'] ?? null ) ? $patch_result['readiness'] : null
        ) ) {
            return new \WP_Error(
                'securwp_firewall_save_failed',
                'Failed to save firewall settings.',
                [ 'status' => 500 ]
            );
        }

        $cloudflare_sync = [ 'status' => 'skipped' ];
        $needs_cloudflare_sync = $this->did_cloudflare_relevant_firewall_settings_change( $previous_settings, $new_settings );
        if ( $requested_allowlist !== null ) {
            $lockout_manager = new LockoutManager();
            $allowlist_result = $lockout_manager->replace_allowlist( $requested_allowlist );
            if ( ! $allowlist_result['success'] ) {
                if ( ! $this->save_interactive_firewall_settings( $previous_settings ) ) {
                    return new \WP_Error(
                        'securwp_firewall_rollback_failed',
                        'Firewall settings rollback failed after the allowlist update error.',
                        [ 'status' => 500 ]
                    );
                }

                $code = sanitize_key( (string) ( $allowlist_result['code'] ?? 'securwp_allowlist_update_failed' ) );
                $status = in_array( $code, [ 'overlap_conflict', 'settings_conflict' ], true ) ? 409 : 400;

                return new \WP_Error(
                    $code,
                    (string) $allowlist_result['message'],
                    [ 'status' => $status ]
                );
            }

            if ( ! empty( $allowlist_result['changed'] ) ) {
                $needs_cloudflare_sync = true;
            }
        }

        if ( $needs_cloudflare_sync ) {
            $cloudflare_sync = $this->maybe_reconcile_cloudflare_edge();
        }

        $blocklist_sync = $this->maybe_sync_blocklist_on_enable( $previous_settings, $new_settings );
        $response_settings = FirewallSettings::redact_sensitive_keys( $this->get_effective_firewall_settings() );
        $waf_status = $this->get_extended_waf_status();
        $response_settings['extended_waf_active'] = (bool) ( $waf_status['active'] ?? false );
        $response_settings['extended_waf_server_type'] = sanitize_text_field( (string) ( $waf_status['server_type'] ?? '' ) );

        return new WP_REST_Response( [
            'message'        => $this->append_cloudflare_sync_message( 'Settings saved successfully.', $cloudflare_sync ),
            'settings'       => $response_settings,
            'blocklist_sync' => $blocklist_sync,
            'cloudflare_sync' => $cloudflare_sync,
        ] );
    }

    public function get_detected_ip() {
        $settings        = $this->get_effective_firewall_settings();
        $resolution      = IpDetector::get_detection_details( $settings );
        $diagnostic      = IpDetector::detect_best_configuration();
        $remote_addr     = IpDetector::get_server_value( 'REMOTE_ADDR' );
        $trusted_proxies = IpDetector::resolve_trusted_proxies( $settings );
        $is_trusted      = ! empty( $trusted_proxies ) && IpUtils::ip_in_range_list( $remote_addr, $trusted_proxies );
        $selected_header = sanitize_text_field( (string) ( $settings['proxy_header'] ?? '' ) );
        $selected_entry  = null;

        if ( '' !== $selected_header ) {
            foreach ( $diagnostic['all_headers'] as $header_entry ) {
                if ( ! is_array( $header_entry ) ) {
                    continue;
                }

                if ( (string) ( $header_entry['header'] ?? '' ) !== $selected_header ) {
                    continue;
                }

                $selected_entry = $header_entry;
                break;
            }
        }

        if ( ! class_exists( 'SecurWP\\Includes\\IpProbe' ) ) {
            require_once dirname( __DIR__, 3 ) . '/includes/IpProbe.php';
        }
        $probe = IpProbe::get_recommendation();

        return new WP_REST_Response( [
            'ip'            => $resolution['ip'],
            'source'        => $resolution['source'],
            'source_key'    => $resolution['source_key'],
            'source_mode'   => $resolution['source_mode'],
            'restored_from' => $resolution['restored_from'],
            'origin_enforcement_mode' => FirewallConfigService::get_origin_enforcement_mode( $settings ),
            'remote_addr'   => $remote_addr,
            'trusted_proxy' => $is_trusted,
            'proxy_preset'  => $settings['trusted_proxy_preset'] ?? 'none',
            'trusted_count' => count( $trusted_proxies ),
            'readiness'     => IpDetector::get_readiness( $settings ),
            'probe'         => $probe ? [
                'status'      => $probe['status'],
                'header'      => $probe['header'],
                'origin_restored' => ! empty( $probe['origin_restored'] ),
                'detected_at' => $probe['detected_at'],
                'fresh'       => IpProbe::is_fresh(),
            ] : null,
            'review'        => [
                'selected_header'         => $selected_header,
                'selected_header_present' => ! empty( $selected_entry['present'] ),
                'selected_header_ip'      => sanitize_text_field( (string) ( $selected_entry['ip'] ?? '' ) ),
                'recommended_header'      => sanitize_text_field( (string) ( $diagnostic['recommended']['header'] ?? '' ) ),
                'recommended_ip'          => sanitize_text_field( (string) ( $diagnostic['recommended']['ip'] ?? '' ) ),
                'recommended_mode'        => sanitize_text_field( (string) ( $diagnostic['recommended_mode'] ?? '' ) ),
                'suggested_preset'        => sanitize_text_field( (string) ( $diagnostic['suggested_preset'] ?? 'none' ) ),
                'has_proxy'               => ! empty( $diagnostic['has_proxy'] ),
                'origin_restored'         => ! empty( $diagnostic['origin_restored'] ),
            ],
        ] );
    }

    /**
     * GET /firewall/ip/diagnose
     * Full diagnostic of all proxy headers and suggested configuration.
     */
    public function diagnose_ip() {
        return new WP_REST_Response( IpDetector::detect_best_configuration() );
    }

    /**
     * GET /firewall/ip-probe (public)
     * Receives the IntelEngine callback and stores the probe recommendation.
     */
    public function handle_ip_probe_callback( WP_REST_Request $request ): WP_REST_Response {
        if ( ! class_exists( 'SecurWP\\Includes\\IpProbe' ) ) {
            require_once dirname( __DIR__, 3 ) . '/includes/IpProbe.php';
        }
        $token  = sanitize_text_field( (string) $request->get_param( 'token' ) );
        $ie_ip  = sanitize_text_field( (string) $request->get_param( 'ip' ) );
        $result = IpProbe::handle_callback( $token, $ie_ip );

        if ( ! $result['ok'] ) {
            return new WP_REST_Response( $result, 400 );
        }

        // Clear the per-request IP cache and the readiness transient so the
        // next real admin page load recomputes both with the updated probe result.
        // Do NOT call cache_readiness() here: REMOTE_ADDR is IntelEngine's IP,
        // not a real visitor, so any cached readiness would be meaningless.
        IpDetector::reset_cache();
        delete_transient( IpDetector::READINESS_TRANSIENT );

        return new WP_REST_Response( $result, 200 );
    }

    /**
     * GET /firewall/ip-probe/status (admin)
     * Returns the current probe recommendation and freshness.
     */
    public function get_ip_probe_status(): WP_REST_Response {
        if ( ! class_exists( 'SecurWP\\Includes\\IpProbe' ) ) {
            require_once dirname( __DIR__, 3 ) . '/includes/IpProbe.php';
        }
        $probe = IpProbe::get_recommendation();
        return new WP_REST_Response( [
            'probe' => $probe ? [
                'status'      => $probe['status'],
                'header'      => $probe['header'],
                'origin_restored' => ! empty( $probe['origin_restored'] ),
                'detected_at' => $probe['detected_at'],
                'fresh'       => IpProbe::is_fresh(),
            ] : null,
        ] );
    }

    /**
     * POST /firewall/ip-probe/trigger (admin)
     * Initiates a fresh IntelEngine probe for this site.
     */
    public function trigger_ip_probe(): WP_REST_Response {
        if ( ! class_exists( 'SecurWP\\Includes\\IpProbe' ) ) {
            require_once dirname( __DIR__, 3 ) . '/includes/IpProbe.php';
        }
        $status = IpProbe::request_probe( true );
        $ok     = $status === 'dispatched';
        return new WP_REST_Response(
            [ 'ok' => $ok, 'message' => $ok ? 'probe_dispatched' : $status ],
            $ok ? 200 : 409
        );
    }

    /**
     * GET /firewall/proxy-presets
     * List available proxy provider presets.
     */
    public function get_proxy_presets() {
        return new WP_REST_Response( [
            'presets' => ProxyPresets::get_all(),
        ] );
    }

    // =========================================================================
    // GeoIP endpoints
    // =========================================================================

    /**
     * GET /firewall/geoip/settings
     * Returns GeoIP-related settings and database info.
     */
    public function get_geoip_settings() {
        $settings = $this->get_merged_settings();
        $downloader = new MaxMindDownloader();
        $db_info    = $downloader->get_db_info();
        $provider_status = FirewallSettings::get_geoip_provider_status();
        $db_info['usable'] = ! empty( $provider_status['local_provider_ready'] );

        $has_key = ! empty( $settings['geoip_license_key'] ) && ! empty( $settings['geoip_account_id'] );

        return new WP_REST_Response( [
            'geoip_enabled'        => (bool) ( $settings['geoip_enabled'] ?? false ) && ! empty( $provider_status['can_enable_country_blocking'] ),
            'geoip_mode'           => $settings['geoip_mode'] ?? 'block_list',
            'geoip_countries'      => $settings['geoip_countries'] ?? '',
            'geoip_key_configured' => $has_key,
            'geoip_key_hint'       => $has_key ? ( strlen( $settings['geoip_license_key'] ) > 4 ? substr( $settings['geoip_license_key'], -4 ) : '****' ) : '',
            'geoip_license_key'    => '',
            'geoip_account_id'     => $settings['geoip_account_id'] ?? '',
            'database'             => $db_info,
            'needs_update'         => $downloader->needs_update(),
            'local_provider_ready' => ! empty( $provider_status['local_provider_ready'] ),
            'cloudflare_edge_rule_ready' => ! empty( $provider_status['cloudflare_edge_rule_ready'] ),
            'cloudflare_origin_header_ready' => ! empty( $provider_status['cloudflare_origin_header_ready'] ),
            'origin_fallback_ready' => ! empty( $provider_status['origin_fallback_ready'] ),
            'cloudflare_detected' => ! empty( $provider_status['cloudflare_detected'] ),
            'server_restored_remote_addr' => ! empty( $provider_status['server_restored_remote_addr'] ),
            'origin_restored_diagnostic' => ! empty( $provider_status['origin_restored_diagnostic'] ),
            'provenance_failure_reason' => (string) ( $provider_status['provenance_failure_reason'] ?? '' ),
            'can_enable_country_blocking' => ! empty( $provider_status['can_enable_country_blocking'] ),
        ] );
    }

    /**
     * POST /firewall/geoip/settings
     * Update GeoIP-related settings.
     */
    public function update_geoip_settings( WP_REST_Request $request ) {
        $params = $request->get_json_params();
        if ( ! is_array( $params ) ) {
            return new WP_REST_Response( [ 'error' => 'Invalid JSON' ], 400 );
        }

        $previous_settings = FirewallSettings::get();
        $patch_result = $this->apply_interactive_settings_patch( $previous_settings, $params, [ 'banned_ips', 'allowlist' ] );
        if ( $patch_result instanceof WP_REST_Response ) {
            return $patch_result;
        }
        $current = $patch_result['settings'];

        if ( ! $this->save_interactive_firewall_settings(
            $current,
            is_array( $patch_result['readiness'] ?? null ) ? $patch_result['readiness'] : null
        ) ) {
            return new WP_REST_Response(
                [
                    'success' => false,
                    'message' => 'Failed to save country blocking settings.',
                ],
                500
            );
        }
        $cloudflare_sync = $this->did_cloudflare_relevant_firewall_settings_change( $previous_settings, $current )
            ? $this->maybe_reconcile_cloudflare_edge()
            : [ 'status' => 'skipped' ];

        return new WP_REST_Response(
            [
                'success'         => true,
                'message'         => $this->append_cloudflare_sync_message( 'Country blocking settings saved successfully.', $cloudflare_sync ),
                'settings'        => FirewallSettings::redact_sensitive_keys( $current ),
                'cloudflare_sync' => $cloudflare_sync,
            ]
        );
    }

    /**
     * POST /firewall/geoip/update-db
     * Download/update the MaxMind GeoLite2-Country database.
     */
    public function update_geoip_db( WP_REST_Request $request ) {
        $params     = $request->get_json_params();
        $settings   = $this->get_merged_settings();
        $account_id  = $params['account_id'] ?? $settings['geoip_account_id'] ?? '';
        $license_key = $params['license_key'] ?? $settings['geoip_license_key'] ?? '';

        if ( empty( $account_id ) || empty( $license_key ) ) {
            return new WP_REST_Response( [ 'error' => 'MaxMind account ID and license key are both required.' ], 400 );
        }

        $downloader = new MaxMindDownloader();
        $result     = $downloader->download( $account_id, $license_key );

        if ( is_wp_error( $result ) ) {
            return new WP_REST_Response( [
                'error' => $result->get_error_message(),
            ], 500 );
        }

        // Save credentials if provided.
        if ( ! empty( $params['account_id'] ) ) {
            SettingsStore::set_field( SettingsStore::COL_FIREWALL, 'geoip_account_id', sanitize_text_field( $params['account_id'] ) );
        }
        if ( ! empty( $params['license_key'] ) ) {
            SettingsStore::set_field( SettingsStore::COL_FIREWALL, 'geoip_license_key', sanitize_text_field( $params['license_key'] ) );
        }

        return new WP_REST_Response( [
            'success'  => true,
            'database' => array_merge(
                $downloader->get_db_info(),
                [ 'usable' => ( new GeoIPService() )->has_usable_mmdb() ]
            ),
        ] );
    }

    /**
     * GET /firewall/geoip/lookup?ip=x.x.x.x
     * Look up country information for an IP address.
     */
    public function geoip_lookup( WP_REST_Request $request ) {
        $ip      = $request->get_param( 'ip' );
        $service = new GeoIPService();
        $result  = $service->lookup_exact_ip( (string) $ip );

        if ( $result === null ) {
            return new WP_REST_Response( [
                'ip'    => $ip,
                'found' => false,
            ] );
        }

        return new WP_REST_Response( [
            'ip'           => $ip,
            'found'        => true,
            'country_code' => $result['country_code'],
            'country_name' => $result['country_name'],
        ] );
    }

    // =========================================================================
    // WHOIS endpoint
    // =========================================================================

    /**
     * GET /firewall/whois?ip=x.x.x.x
     * Perform a WHOIS/RDAP lookup for an IP address.
     */
    public function whois_lookup( WP_REST_Request $request ) {
        $ip = $request->get_param( 'ip' );

        if ( ! class_exists( 'SecurWP\\Modules\\Firewall\\WHOIS\\WHOISLookup' ) ) {
            require_once dirname( __DIR__ ) . '/WHOIS/WHOISLookup.php';
        }

        $whois  = new WHOISLookup();
        $result = $whois->lookup( $ip );

        if ( is_wp_error( $result ) ) {
            return new WP_REST_Response( [
                'error' => $result->get_error_message(),
            ], 400 );
        }

        return new WP_REST_Response( $result );
    }

    // =========================================================================
    // Blocklist endpoints
    // =========================================================================

    /**
     * GET /firewall/blocklist/settings
     * Returns blocklist-related settings.
     */
    public function get_blocklist_settings() {
        $settings = $this->get_merged_settings();

        return new WP_REST_Response( [
            'blocklist_enabled'              => (bool) ( $settings['blocklist_enabled'] ?? false ),
        ] );
    }

    /**
     * POST /firewall/blocklist/settings
     * Update blocklist-related settings.
     */
    public function update_blocklist_settings( WP_REST_Request $request ) {
        $params = $request->get_json_params();
        if ( ! is_array( $params ) ) {
            return new WP_REST_Response( [ 'error' => 'Invalid JSON' ], 400 );
        }

        $current = FirewallSettings::get();
        $previous_settings = $current;
        $patch_result = $this->apply_interactive_settings_patch( $current, $params, [ 'banned_ips', 'allowlist' ] );
        if ( $patch_result instanceof WP_REST_Response ) {
            return $patch_result;
        }
        $current = $patch_result['settings'];

        if ( ! $this->save_interactive_firewall_settings(
            $current,
            is_array( $patch_result['readiness'] ?? null ) ? $patch_result['readiness'] : null
        ) ) {
            return new WP_REST_Response(
                [
                    'success' => false,
                    'message' => 'Failed to save community blocklist settings.',
                ],
                500
            );
        }
        $blocklist_sync = $this->maybe_sync_blocklist_on_enable( $previous_settings, $current );

        return new WP_REST_Response( [
            'success'        => true,
            'settings'       => FirewallSettings::redact_sensitive_keys( $current ),
            'blocklist_sync' => $blocklist_sync,
        ] );
    }

    /**
     * POST /firewall/blocklist/sync
     * Trigger an immediate blocklist sync.
     */
    public function sync_blocklist() {
        $settings = $this->get_merged_settings();
        $service  = new BlocklistService( $settings );
        $stats    = $service->sync();

        return new WP_REST_Response( [
            'success' => true,
            'stats'   => $stats,
        ] );
    }

    /**
     * GET /firewall/blocklist/stats
     * Get blocklist statistics.
     */
    public function get_blocklist_stats() {
        $settings = $this->get_merged_settings();
        $service  = new BlocklistService( $settings );
        $stats    = $service->get_stats();
        if ( ! isset( $stats['total_entries'] ) ) {
            $stats['total_entries'] = (int) ( $stats['entry_count'] ?? 0 );
        }

        return new WP_REST_Response( $stats );
    }

    /**
     * GET /firewall/extended-protection
     * Get the status of extended protection features.
     */
    public function get_extended_protection_status() {
        $settings = $this->get_merged_settings();
        $waf_status = $this->serialize_extended_waf_status( $this->get_extended_waf_status() );

        return new WP_REST_Response( [
            'active'            => (bool) ( $waf_status['active'] ?? false ),
            'runtime_active'    => (bool) ( $waf_status['runtime_active'] ?? false ),
            'installed_on_disk' => ! empty( $waf_status['installed_on_disk'] ),
            'pending_activation' => ! empty( $waf_status['pending_activation'] ),
            'pending_message'   => isset( $waf_status['pending_message'] ) ? sanitize_text_field( (string) $waf_status['pending_message'] ) : null,
            'server_type'       => sanitize_text_field( (string) ( $waf_status['server_type'] ?? '' ) ),
            'bootstrap_exists'  => ! empty( $waf_status['bootstrap_exists'] ),
            'config_exists'     => ! empty( $waf_status['config_exists'] ),
            'prepend_path'      => sanitize_text_field( (string) ( $waf_status['prepend_path'] ?? '' ) ),
            'stale_runtime_prepend' => ! empty( $waf_status['stale_runtime_prepend'] ),
            'conflict_detected' => ! empty( $waf_status['conflict_detected'] ),
            'rate_limiting' => [
                'enabled'            => (bool) ( $settings['rate_limit_enabled'] ?? false ),
                'requests_per_min'   => (int) ( $settings['rate_limit_requests'] ?? 60 ),
                '404_per_min'        => (int) ( $settings['rate_limit_404'] ?? 10 ),
            ],
            'community_blocklist' => [
                'enabled'    => (bool) ( $settings['blocklist_enabled'] ?? false ),
            ],
            'country_blocking' => [
                'enabled'   => (bool) ( $settings['geoip_enabled'] ?? false ),
                'mode'      => $settings['geoip_mode'] ?? 'block_list',
                'countries' => $settings['geoip_countries'] ?? '',
            ],
        ] );
    }

    /**
     * POST /firewall/extended-protection
     * Update extended protection settings (rate limiting, blocklist, geoip).
     */
    public function update_extended_protection( WP_REST_Request $request ) {
        $params = $request->get_json_params();
        if ( ! is_array( $params ) ) {
            return new WP_REST_Response( [ 'error' => 'Invalid JSON' ], 400 );
        }

        $action = sanitize_key( (string) ( $params['action'] ?? '' ) );
        if ( $action === 'install' ) {
            if ( ! class_exists( '\SecurWP\Modules\Firewall\ExtendedProtection\Manager' ) ) {
                return new WP_REST_Response( [ 'error' => 'Extended protection manager is unavailable.' ], 500 );
            }

            $result = ExtendedProtectionManager::install();
            if ( empty( $result['success'] ) ) {
                $status = $this->serialize_extended_waf_status( ExtendedProtectionManager::get_status() );
                $message = sanitize_text_field( (string) ( $result['error'] ?? 'Failed to install extended protection.' ) );

                if ( ! empty( $status['conflict_detected'] ) ) {
                    return new WP_REST_Response(
                        [
                            'message' => $message,
                            'status'  => $status,
                        ],
                        409
                    );
                }

                return new WP_REST_Response(
                    [
                        'message' => $message,
                        'status'  => $status,
                    ],
                    500
                );
            }

            $status = $this->serialize_extended_waf_status( ExtendedProtectionManager::get_status() );

            return new WP_REST_Response( [
                'success'  => true,
                'message'  => $this->build_extended_protection_message( $status, 'install' ),
                'method'   => sanitize_key( (string) ( $result['method'] ?? '' ) ),
                'active'   => (bool) ( $status['active'] ?? false ),
                'status'   => $status,
            ] );
        }

        if ( $action === 'uninstall' ) {
            if ( ! class_exists( '\SecurWP\Modules\Firewall\ExtendedProtection\Manager' ) ) {
                return new WP_REST_Response( [ 'error' => 'Extended protection manager is unavailable.' ], 500 );
            }

            $result = ExtendedProtectionManager::uninstall();
            if ( empty( $result['success'] ) ) {
                return new WP_REST_Response(
                    [ 'message' => sanitize_text_field( (string) ( $result['error'] ?? 'Failed to remove extended protection.' ) ) ],
                    500
                );
            }

            $status = $this->serialize_extended_waf_status( ExtendedProtectionManager::get_status() );

            return new WP_REST_Response( [
                'success' => true,
                'message' => $this->build_extended_protection_message( $status, 'uninstall' ),
                'active'  => (bool) ( $status['active'] ?? false ),
                'status'  => $status,
            ] );
        }

        $current = FirewallSettings::get();
        $previous_settings = $current;
        $patch_result = $this->apply_interactive_settings_patch( $current, $params, [ 'banned_ips', 'allowlist' ] );
        if ( $patch_result instanceof WP_REST_Response ) {
            return $patch_result;
        }
        $current = $patch_result['settings'];

        if ( ! $this->save_interactive_firewall_settings(
            $current,
            is_array( $patch_result['readiness'] ?? null ) ? $patch_result['readiness'] : null
        ) ) {
            return new WP_REST_Response(
                [
                    'success' => false,
                    'message' => 'Failed to save extended protection settings.',
                ],
                500
            );
        }
        $blocklist_sync = $this->maybe_sync_blocklist_on_enable( $previous_settings, $current );
        $status = $this->get_extended_waf_status();

        return new WP_REST_Response( [
            'success'        => true,
            'settings'       => FirewallSettings::redact_sensitive_keys( $current ),
            'active'         => (bool) ( $status['active'] ?? false ),
            'status'         => $status,
            'blocklist_sync' => $blocklist_sync,
        ] );
    }

    // =========================================================================
    // IP Lockout management endpoints
    // =========================================================================

    /**
     * GET /firewall/lockouts
     * Returns all IP lockout entries (bans + allowlist) as structured data.
     */
    public function get_lockouts( WP_REST_Request $request ) {
        $type    = $request->get_param( 'type' );
        $manager = new LockoutManager();
        $entries = $manager->get_all( $type );
        $counts  = $manager->get_counts();

        return new WP_REST_Response( [
            'entries' => $entries,
            'counts'  => $counts,
        ] );
    }

    /**
     * POST /firewall/lockouts
     * Add an IP to the ban or allow list.
     */
    public function add_lockout( WP_REST_Request $request ) {
        $params = $request->get_json_params();
        if ( ! is_array( $params ) ) {
            return new \WP_Error( 'securwp_lockout_invalid_json', 'Invalid JSON.', [ 'status' => 400 ] );
        }

        $ip   = sanitize_text_field( trim( $params['ip'] ?? '' ) );
        $type = in_array( $params['type'] ?? '', [ 'ban', 'allow' ], true ) ? $params['type'] : 'ban';

        if ( '' === $ip ) {
            return new \WP_Error( 'securwp_lockout_missing_ip', 'IP address is required.', [ 'status' => 400 ] );
        }

        $manager = new LockoutManager();
        $result  = $manager->add( $ip, $type, [
            'source'           => sanitize_key( $params['source'] ?? 'manual' ),
            'reason'           => sanitize_text_field( $params['reason'] ?? '' ),
            'duration'         => (int) ( $params['duration'] ?? 0 ),
            'replace_conflict' => ! empty( $params['replace_conflict'] ),
        ] );

        if ( ! $result['success'] ) {
            $code = sanitize_key( (string) ( $result['code'] ?? 'securwp_lockout_error' ) );
            $status = in_array( $code, [ 'opposite_list_conflict', 'overlap_conflict', 'settings_conflict' ], true ) ? 409 : 400;
            return new \WP_Error( $code, (string) $result['message'], [ 'status' => $status ] );
        }
        $cloudflare_sync = $this->maybe_reconcile_cloudflare_edge();

        return new WP_REST_Response( [
            'success' => true,
            'message' => $this->append_cloudflare_sync_message( (string) $result['message'], $cloudflare_sync ),
            'entries' => $manager->get_all(),
            'counts'  => $manager->get_counts(),
            'cloudflare_sync' => $cloudflare_sync,
        ] );
    }

    /**
     * POST /firewall/lockouts/remove
     * Remove an IP from the ban or allow list.
     */
    public function remove_lockout( WP_REST_Request $request ) {
        $params = $request->get_json_params();
        if ( ! is_array( $params ) ) {
            return new \WP_Error( 'securwp_lockout_invalid_json', 'Invalid JSON.', [ 'status' => 400 ] );
        }

        $ip   = sanitize_text_field( trim( $params['ip'] ?? '' ) );
        $type = in_array( $params['type'] ?? '', [ 'ban', 'allow' ], true ) ? $params['type'] : 'ban';

        if ( '' === $ip ) {
            return new \WP_Error( 'securwp_lockout_missing_ip', 'IP address is required.', [ 'status' => 400 ] );
        }

        $manager = new LockoutManager();
        $result  = $manager->remove( $ip, $type );

        if ( ! $result['success'] ) {
            return new \WP_Error( 'securwp_lockout_remove_failed', (string) $result['message'], [ 'status' => 400 ] );
        }
        $cloudflare_sync = $this->maybe_reconcile_cloudflare_edge();

        return new WP_REST_Response( [
            'success' => true,
            'message' => $this->append_cloudflare_sync_message( (string) $result['message'], $cloudflare_sync ),
            'entries' => $manager->get_all(),
            'counts'  => $manager->get_counts(),
            'cloudflare_sync' => $cloudflare_sync,
        ] );
    }

    /**
     * POST /firewall/lockouts/purge-expired
     * Remove all expired bans.
     */
    public function purge_expired_lockouts() {
        $manager = new LockoutManager();
        $purged  = $manager->purge_expired();
        $cloudflare_sync = $purged > 0 ? $this->maybe_reconcile_cloudflare_edge() : [ 'status' => 'skipped' ];

        return new WP_REST_Response( [
            'success' => true,
            'purged'  => $purged,
            'message' => $purged > 0 ? $this->append_cloudflare_sync_message( sprintf( 'Purged %d expired entries.', $purged ), $cloudflare_sync ) : '',
            'entries' => $manager->get_all(),
            'counts'  => $manager->get_counts(),
            'cloudflare_sync' => $cloudflare_sync,
        ] );
    }

    // =========================================================================
    // Helpers
    // =========================================================================

    /**
     * Trigger an immediate sync when blocklist is newly enabled.
     *
     * @param array<string,mixed> $previous_settings
     * @param array<string,mixed> $new_settings
     * @return array{triggered: bool, success: bool, stats?: array<string,mixed>, error?: string}
     */
    private function maybe_sync_blocklist_on_enable( array $previous_settings, array $new_settings ): array {
        $was_enabled = ! empty( $previous_settings['blocklist_enabled'] );
        $is_enabled  = ! empty( $new_settings['blocklist_enabled'] );
        if ( $was_enabled || ! $is_enabled ) {
            return [
                'triggered' => false,
                'success'   => true,
            ];
        }

        try {
            $stats = ( new BlocklistService( $new_settings ) )->sync();

            return [
                'triggered' => true,
                'success'   => true,
                'stats'     => is_array( $stats ) ? $stats : [],
            ];
        } catch ( \Throwable $throwable ) {
            return [
                'triggered' => true,
                'success'   => false,
                'error'     => sanitize_text_field( $throwable->getMessage() ),
            ];
        }
    }

    /**
     * Reconcile Cloudflare edge state immediately for admin-driven rule changes.
     *
     * @return array{status:string,message?:string}
     */
    private function maybe_reconcile_cloudflare_edge(): array {
        if ( ! class_exists( '\SecurWP\Modules\Cloudflare\Manager' ) ) {
            return [ 'status' => 'skipped' ];
        }

        $manager  = new CloudflareManager();
        $settings = $manager->get_admin_settings();
        if ( empty( $settings['enabled'] ) || empty( $settings['configured'] ) ) {
            return [ 'status' => 'skipped' ];
        }

        $result = $manager->reconcile( true );
        if ( is_wp_error( $result ) ) {
            return [
                'status'  => 'failed',
                'message' => sanitize_text_field( $result->get_error_message() ),
            ];
        }

        if ( ! empty( $result['busy'] ) ) {
            return [
                'status'  => 'pending',
                'message' => sanitize_text_field( (string) ( $result['message'] ?? 'Cloudflare synchronization is already running.' ) ),
            ];
        }

        if ( ! empty( $result['pending'] ) ) {
            return [
                'status'  => 'pending',
                'message' => sanitize_text_field( (string) ( $result['message'] ?? 'Cloudflare update is still processing.' ) ),
            ];
        }

        if ( ! empty( $result['warning'] ) ) {
            return [
                'status'  => 'warning',
                'message' => sanitize_text_field( (string) ( $result['message'] ?? 'Cloudflare update completed with a warning.' ) ),
            ];
        }

        return [ 'status' => 'synced' ];
    }

    /**
     * @param array{status:string,message?:string} $cloudflare_sync
     */
    private function append_cloudflare_sync_message( string $message, array $cloudflare_sync ): string {
        $status = (string) ( $cloudflare_sync['status'] ?? 'skipped' );
        $sync_message = trim( (string) ( $cloudflare_sync['message'] ?? '' ) );

        if ( $status === 'failed' ) {
            return $message . ( $sync_message !== ''
                ? ' Local firewall updated, but Cloudflare sync failed: ' . $sync_message
                : ' Local firewall updated, but Cloudflare sync failed.'
            );
        }

        if ( $status === 'pending' ) {
            return $message . ( $sync_message !== ''
                ? ' Cloudflare update is still processing: ' . $sync_message
                : ' Cloudflare update is still processing.'
            );
        }

        if ( $status === 'warning' ) {
            return $message . ( $sync_message !== ''
                ? ' Cloudflare sync warning: ' . $sync_message
                : ' Cloudflare update completed with a warning.'
            );
        }

        return $message;
    }

    /**
     * @param array<string,mixed> $current
     * @param array<string,mixed> $params
     * @param array<int,string>   $locked_keys
     * @return array{settings: array<string,mixed>, readiness: ?array<string,mixed>}|WP_REST_Response
     */
    private function apply_interactive_settings_patch( array $current, array $params, array $locked_keys ) {
        try {
            return FirewallConfigService::apply_patch(
                $current,
                $params,
                $locked_keys,
                FirewallConfigService::CONTEXT_INTERACTIVE
            );
        } catch ( \InvalidArgumentException $exception ) {
            return new WP_REST_Response(
                [
                    'error' => sanitize_text_field( $exception->getMessage() ),
                ],
                400
            );
        }
    }

    /**
     * Persist firewall settings and immediately sync the effective
     * origin-enforcement verdict for non-interactive consumers.
     *
     * @param array<string,mixed>      $settings
     * @param array<string,mixed>|null $readiness
     */
    private function save_interactive_firewall_settings( array $settings, ?array $readiness = null ): bool {
        $settings = FirewallSettings::sanitize( $settings );

        if ( ! SettingsStore::save( SettingsStore::COL_FIREWALL, $settings ) ) {
            return false;
        }

        if ( ! is_array( $readiness ) ) {
            $state = FirewallConfigService::reconcile_runtime_mode_from_sanitized( $settings );
            $settings = $state['settings'];
            $readiness = is_array( $state['readiness'] ?? null ) ? $state['readiness'] : null;
        }

        FirewallConfigService::sync_effective_mode_state(
            $settings,
            FirewallConfigService::get_origin_enforcement_mode( $settings ),
            $readiness
        );

        return true;
    }

    /**
     * @return array<string,mixed>
     */
    private function get_extended_waf_status(): array {
        if ( ! class_exists( '\SecurWP\Modules\Firewall\ExtendedProtection\Manager' ) ) {
            return [
                'active'      => false,
                'server_type' => '',
            ];
        }

        $status = ExtendedProtectionManager::get_status();
        if ( ! is_array( $status ) ) {
            return [
                'active'      => false,
                'server_type' => '',
            ];
        }

        return $status;
    }

    /**
     * Load firewall settings with effective origin mode reconciled for the
     * current request context.
     *
     * This keeps settings reads aligned with the same readiness verdict used
     * by runtime enforcement instead of trusting a potentially stale saved mode.
     *
     * @return array<string,mixed>
     */
    private function get_effective_firewall_settings(): array {
        return FirewallConfigService::load_effective_settings();
    }

    /**
     * Sanitize the extended WAF status payload for REST responses.
     *
     * @param array<string, mixed> $waf_status Raw status from the manager.
     * @return array<string, mixed>
     */
    private function serialize_extended_waf_status( array $waf_status ): array {
        return [
            'active'             => ! empty( $waf_status['active'] ),
            'runtime_active'     => ! empty( $waf_status['runtime_active'] ),
            'installed_on_disk'  => ! empty( $waf_status['installed_on_disk'] ),
            'pending_activation' => ! empty( $waf_status['pending_activation'] ),
            'pending_message'    => isset( $waf_status['pending_message'] ) ? sanitize_text_field( (string) $waf_status['pending_message'] ) : null,
            'server_type'        => sanitize_text_field( (string) ( $waf_status['server_type'] ?? '' ) ),
            'bootstrap_exists'   => ! empty( $waf_status['bootstrap_exists'] ),
            'config_exists'      => ! empty( $waf_status['config_exists'] ),
            'prepend_path'       => sanitize_text_field( (string) ( $waf_status['prepend_path'] ?? '' ) ),
            'stale_runtime_prepend' => ! empty( $waf_status['stale_runtime_prepend'] ),
            'conflict_detected'  => ! empty( $waf_status['conflict_detected'] ),
        ];
    }

    /**
     * Build the canonical user-facing message for WAF install/uninstall transitions.
     *
     * @param array<string, mixed> $status
     * @param 'install'|'uninstall' $action
     */
    private function build_extended_protection_message( array $status, string $action ): string {
        if ( $action === 'install' ) {
            if ( ! empty( $status['active'] ) ) {
                return 'Server-level WAF installed and active.';
            }

            if ( ! empty( $status['pending_activation'] ) ) {
                $pending_message = isset( $status['pending_message'] ) ? trim( (string) $status['pending_message'] ) : '';
                return $pending_message !== '' ? $pending_message : 'Server-level WAF installed. Activation is pending.';
            }

            if ( ! empty( $status['installed_on_disk'] ) ) {
                return 'Server-level WAF installed. Runtime activation is still being verified.';
            }

            return 'Server-level WAF install completed.';
        }

        if ( ! empty( $status['stale_runtime_prepend'] ) ) {
            return 'Server-level WAF was removed from configuration. PHP may still be serving a cached startup file until the current runtime refreshes.';
        }

        return 'Server-level WAF removed.';
    }

    /**
     * Cloudflare mirrors only a subset of local firewall settings directly.
     *
     * @param array<string,mixed> $settings
     * @return array<string,mixed>
     */
    private function get_cloudflare_relevant_firewall_state( array $settings ): array {
        $settings = FirewallSettings::sanitize( $settings );

        return [
            'geoip_enabled'        => ! empty( $settings['geoip_enabled'] ),
            'geoip_mode'           => (string) ( $settings['geoip_mode'] ?? 'block_list' ),
            'geoip_countries'      => (string) ( $settings['geoip_countries'] ?? '' ),
            'banned_user_agents'   => (string) ( $settings['banned_user_agents'] ?? '' ),
            'trusted_user_agents'  => (string) ( $settings['trusted_user_agents'] ?? '' ),
        ];
    }

    /**
     * Determine whether a local firewall edit requires Cloudflare edge re-sync.
     *
     * @param array<string,mixed> $previous_settings
     * @param array<string,mixed> $current_settings
     */
    private function did_cloudflare_relevant_firewall_settings_change( array $previous_settings, array $current_settings ): bool {
        return $this->get_cloudflare_relevant_firewall_state( $previous_settings ) !== $this->get_cloudflare_relevant_firewall_state( $current_settings );
    }

    /**
     * Redact sensitive keys from settings before sending to the client.
     */
    private function get_merged_settings(): array {
        return FirewallSettings::get();
    }

}
