<?php

declare(strict_types=1);

namespace SiteFort\Modules\Hardening;

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

use WP_REST_Server;
use WP_REST_Request;
use WP_REST_Response;
use WP_Error;
use SiteFort\Includes\AdminFilesystemCredentials;
use SiteFort\Includes\FilesystemMutation;
use SiteFort\Includes\SettingsStore;
use SiteFort\Modules\AuditLog\AuditLog;
use SiteFort\Modules\Firewall\IpBlocking;

/**
 * Hardening module REST API routes.
 */
class HardeningApi {

    private const CAPTCHA_CONTEXT_KEYS = [
        'captcha_enabled',
        'captcha_provider',
        'captcha_site_key',
        'captcha_secret_key',
        'captcha_secret_configured',
        'captcha_threshold',
        'captcha_provider_profiles',
    ];

    private Hardening $hardening;

    public function __construct( Hardening $hardening ) {
        $this->hardening = $hardening;
    }

    public function register_routes(): void {
        $ns = 'sitefort/v1';

        register_rest_route( $ns, '/hardening/settings', [
            [
                'methods'             => WP_REST_Server::READABLE,
                'callback'            => [ $this, 'get_settings' ],
                'permission_callback' => [ $this, 'check_permission' ],
            ],
            [
                'methods'             => WP_REST_Server::EDITABLE,
                'callback'            => [ $this, 'update_settings' ],
                'permission_callback' => [ $this, 'check_permission' ],
            ],
        ] );

        register_rest_route( $ns, '/hardening/php-block-snippets', [
            [
                'methods'             => WP_REST_Server::READABLE,
                'callback'            => [ $this, 'get_php_block_snippets' ],
                'permission_callback' => [ $this, 'check_permission' ],
            ],
        ] );

        register_rest_route( $ns, '/hardening/file-permissions', [
            [
                'methods'             => WP_REST_Server::READABLE,
                'callback'            => [ $this, 'get_file_permissions' ],
                'permission_callback' => [ $this, 'check_permission' ],
            ],
        ] );

        register_rest_route( $ns, '/hardening/rest-api-endpoints', [
            [
                'methods'             => WP_REST_Server::READABLE,
                'callback'            => [ $this, 'get_rest_api_endpoints' ],
                'permission_callback' => [ $this, 'check_permission' ],
            ],
        ] );

        register_rest_route( $ns, '/hardening/login-protection/overview', [
            [
                'methods'             => WP_REST_Server::READABLE,
                'callback'            => [ $this, 'get_login_protection_overview' ],
                'permission_callback' => [ $this, 'check_permission' ],
            ],
        ] );

        register_rest_route( $ns, '/hardening/config-rules', [
            [
                'methods'             => WP_REST_Server::READABLE,
                'callback'            => [ $this, 'get_config_rules' ],
                'permission_callback' => [ $this, 'check_permission' ],
            ],
        ] );

        register_rest_route( $ns, '/hardening/server-rule-state', [
            [
                'methods'             => WP_REST_Server::READABLE,
                'callback'            => [ $this, 'get_server_rule_state' ],
                'permission_callback' => [ $this, 'check_permission' ],
            ],
        ] );

        register_rest_route( $ns, '/hardening/wp-config-rule-state', [
            [
                'methods'             => WP_REST_Server::READABLE,
                'callback'            => [ $this, 'get_wp_config_rule_state' ],
                'permission_callback' => [ $this, 'check_permission' ],
            ],
        ] );

        register_rest_route( $ns, '/hardening/server-rule-state/verify', [
            [
                'methods'             => WP_REST_Server::CREATABLE,
                'callback'            => [ $this, 'verify_server_rule_state' ],
                'permission_callback' => [ $this, 'check_permission' ],
            ],
        ] );

        register_rest_route( $ns, '/hardening/lockout/unlock-ip', [
            'methods'             => WP_REST_Server::CREATABLE,
            'callback'            => [ $this, 'unlock_ip' ],
            'permission_callback' => [ $this, 'check_permission' ],
        ] );

        register_rest_route( $ns, '/hardening/lockout/unlock-user', [
            'methods'             => WP_REST_Server::CREATABLE,
            'callback'            => [ $this, 'unlock_user' ],
            'permission_callback' => [ $this, 'check_permission' ],
        ] );

        register_rest_route( $ns, '/hardening/lockout/unlock-all', [
            'methods'             => WP_REST_Server::CREATABLE,
            'callback'            => [ $this, 'unlock_all' ],
            'permission_callback' => [ $this, 'check_permission' ],
        ] );
    }

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

    /**
     * Build REST-safe hardening settings payload.
     *
     * @param string              $context Response context: full, minimal, server, obscurity, headers, captcha.
     * @param array<string,mixed> $runtime_overrides Optional already-computed runtime states.
     */
    private function build_response_settings( string $context = 'full', array $runtime_overrides = [] ): array {
        $settings = $this->hardening->get_settings();
        if ( ! is_array( $settings ) ) {
            $settings = [];
        }
        $context = $this->normalize_response_context( $context );
        $active_provider = Hardening::normalize_captcha_provider_name( (string) ( $settings['captcha_provider'] ?? 'recaptcha_v3' ) );
        $settings['captcha_provider'] = $active_provider;
        $settings['captcha_provider_profiles'] = Hardening::get_captcha_provider_profiles_response( $settings );
        $settings['captcha_site_key'] = Hardening::get_captcha_site_key_for_provider( $settings, $active_provider );
        $settings['captcha_secret_key'] = '';
        $settings['captcha_secret_configured'] = Hardening::has_captcha_secret( $active_provider );
        if ( 'captcha' === $context ) {
            return $this->filter_response_settings( $settings, self::CAPTCHA_CONTEXT_KEYS );
        }
        $settings['_permalink_structure'] = get_option( 'permalink_structure' );
        $settings['_server_type'] = \SiteFort\Includes\ServerConfigWriter::detect_server();
        if ( $this->response_context_includes_server_rule_state( $context ) ) {
            $settings['_server_rule_state'] = isset( $runtime_overrides['server_rule_state'] ) && is_array( $runtime_overrides['server_rule_state'] )
                ? $runtime_overrides['server_rule_state']
                : \SiteFort\Includes\ServerConfigWriter::get_state();
        }
        if ( $this->response_context_includes_wp_config_rule_state( $context ) ) {
            $settings['_wp_config_rule_state'] = isset( $runtime_overrides['wp_config_rule_state'] ) && is_array( $runtime_overrides['wp_config_rule_state'] )
                ? $runtime_overrides['wp_config_rule_state']
                : $this->hardening->get_wp_config_rule_state( $settings );
        }
        return $settings;
    }

    public function get_settings( WP_REST_Request $request ): WP_REST_Response {
        $settings = $this->build_response_settings( $this->get_response_context_from_request( $request ) );
        return new WP_REST_Response( $settings, 200 );
    }

    private function get_response_context_from_request( WP_REST_Request $request ): string {
        $context = $request->get_param( 'context' );
        if ( ! is_string( $context ) || '' === trim( $context ) ) {
            $context = $request->get_param( '_response_context' );
        }

        return $this->normalize_response_context( is_string( $context ) ? $context : 'full' );
    }

    private function normalize_response_context( string $context ): string {
        $context = sanitize_key( $context );
        return in_array( $context, [ 'full', 'minimal', 'server', 'obscurity', 'headers', 'captcha' ], true ) ? $context : 'full';
    }

    /**
     * @param array<string,mixed> $settings
     * @param array<int,string>   $keys
     * @return array<string,mixed>
     */
    private function filter_response_settings( array $settings, array $keys ): array {
        return array_intersect_key( $settings, array_flip( $keys ) );
    }

    private function response_context_includes_server_rule_state( string $context ): bool {
        return in_array( $context, [ 'full', 'server' ], true );
    }

    private function response_context_includes_wp_config_rule_state( string $context ): bool {
        return in_array( $context, [ 'full', 'obscurity' ], true );
    }

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

        $response_context = $this->get_response_context_from_request( $request );
        $sanitized    = Hardening::sanitize_settings_patch( $params );
        $old_settings = $this->hardening->get_settings();

        if ( $this->patch_requires_wp_config_write( $sanitized, $old_settings ) ) {
            $fs_error = AdminFilesystemCredentials::ensure_rest_access(
                $request,
                'wp_config_rules',
                [ $this, 'can_update_wp_config_without_credentials' ]
            );
            if ( $fs_error ) {
                return $fs_error;
            }
        }

        // Reject enabling rename login URL when plain permalinks are active.
        if ( ! empty( $sanitized['rename_login_url_enabled'] ) && '' === get_option( 'permalink_structure' ) ) {
            return new WP_REST_Response( [
                'success' => false,
                'error'   => __( 'Login URL renaming requires pretty permalinks. Go to Settings → Permalinks and select any structure other than "Plain".', 'sitefort' ),
            ], 400 );
        }

        $candidate_settings = array_merge( $old_settings, $sanitized );
        $login_error_code   = Hardening::validate_login_slug_configuration( $candidate_settings );
        if ( $login_error_code !== null ) {
            return new WP_REST_Response( [
                'success' => false,
                'error'   => Hardening::get_login_slug_validation_message( $login_error_code ),
            ], 400 );
        }

        // Detect page/post slug conflicts for login and register slugs.
        $warnings = [];
        $slug_fields = [ 'rename_login_url_slug', 'rename_login_url_register_slug' ];
        foreach ( $slug_fields as $field ) {
            if ( empty( $sanitized[ $field ] ) ) {
                continue;
            }
            $slug     = $sanitized[ $field ];
            if ( $field === 'rename_login_url_register_slug' && strtolower( (string) $slug ) === 'wp-signup.php' ) {
                continue;
            }
            $conflict = get_page_by_path( $slug, OBJECT, [ 'page', 'post' ] );
            if ( $conflict ) {
                $warnings[] = sprintf(
                    /* translators: 1: slug, 2: post type, 3: post title */
                    __( 'Warning: "%1$s" conflicts with an existing %2$s ("%3$s"). The login slug will take priority, but the %2$s will become inaccessible at that URL.', 'sitefort' ),
                    $slug,
                    $conflict->post_type,
                    $conflict->post_title
                );
            }
        }

        $server_config_keys = [
            'disable_directory_browsing',
            'block_php_uploads',
            'block_php_plugins',
            'block_php_themes',
            'disable_xmlrpc',
            'block_sensitive_files',
        ];
        $server_config_changed = false;
        foreach ( $server_config_keys as $k ) {
            if ( array_key_exists( $k, $sanitized ) && ( $old_settings[ $k ] ?? null ) !== $sanitized[ $k ] ) {
                $server_config_changed = true;
                break;
            }
        }

        $updated = $this->hardening->update_settings( $sanitized );

        if ( $updated ) {
            // Keys whose values must never appear in the audit log.
            $redacted_keys = [ 'captcha_secret_key', 'captcha_site_key' ];

            $changes = [];
            foreach ( $sanitized as $key => $new_val ) {
                $old_val = $old_settings[ $key ] ?? null;
                if ( $old_val !== $new_val ) {
                    if ( in_array( $key, $redacted_keys, true ) ) {
                        $changes[ $key ] = [
                            'from' => $old_val ? '(redacted)' : '',
                            'to'   => $new_val ? '(redacted)' : '',
                        ];
                    } else {
                        $changes[ $key ] = [ 'from' => $old_val, 'to' => $new_val ];
                    }
                }
            }
            AuditLog::add_log(
                'Hardening Settings Changed',
                [ 'changes' => $changes ],
                'warning',
                'hardening'
            );
        }

        $message = $updated
            ? __( 'Hardening settings saved successfully.', 'sitefort' )
            : __( 'Settings are already up to date.', 'sitefort' );

        if ( $updated && $server_config_changed ) {
            $server_rule_state = \SiteFort\Includes\ServerConfigWriter::get_state();
            $server_mode       = is_string( $server_rule_state['mode'] ?? null ) ? (string) $server_rule_state['mode'] : 'none';
            $server_message    = is_string( $server_rule_state['message'] ?? null ) ? trim( (string) $server_rule_state['message'] ) : '';
            $server_banner     = is_array( $server_rule_state['banner'] ?? null ) ? (array) $server_rule_state['banner'] : [];
            $server_warnings   = [];
            $server_banner_shown = ! empty( $server_banner['show'] );

            if ( ! $server_banner_shown ) {
                if (
                    $server_message !== ''
                    && in_array( $server_mode, [ 'manual_only', 'sync_pending', 'sync_failed', 'repair_pending', 'repair_required', 'removal_pending', 'removal_failed', 'removal_required' ], true )
                ) {
                    $server_warnings[] = $server_message;
                }

                if ( is_string( $server_banner['message'] ?? null ) && trim( (string) $server_banner['message'] ) !== '' ) {
                    $server_warnings[] = trim( (string) $server_banner['message'] );
                }
            }

            foreach ( array_values( array_unique( $server_warnings ) ) as $warning ) {
                $warnings[] = $warning;
            }
        }

        if ( array_key_exists( 'disable_file_editor', $sanitized ) ) {
            $wp_config_rule_state = $this->hardening->get_wp_config_rule_state( $this->hardening->get_settings() );
            $wp_config_mode       = is_string( $wp_config_rule_state['mode'] ?? null ) ? (string) $wp_config_rule_state['mode'] : 'none';
            $wp_config_message    = is_string( $wp_config_rule_state['message'] ?? null ) ? trim( (string) $wp_config_rule_state['message'] ) : '';

            if (
                $wp_config_message !== ''
                && in_array( $wp_config_mode, [ 'runtime', 'sync_pending', 'sync_failed', 'repair_pending', 'repair_required', 'removal_pending', 'removal_failed', 'removal_required' ], true )
            ) {
                $warnings[] = $wp_config_message;
            }
        }

        $runtime_overrides = [];
        if ( isset( $server_rule_state ) && is_array( $server_rule_state ) ) {
            $runtime_overrides['server_rule_state'] = $server_rule_state;
        }
        if ( isset( $wp_config_rule_state ) && is_array( $wp_config_rule_state ) ) {
            $runtime_overrides['wp_config_rule_state'] = $wp_config_rule_state;
        }

        $response_data = [
            'success'  => true,
            'message'  => $message,
            'settings' => $this->build_response_settings( $response_context, $runtime_overrides ),
        ];

        if ( ! empty( $warnings ) ) {
            $response_data['warnings'] = $warnings;
        }

        return new WP_REST_Response( $response_data, 200 );
    }

    /**
     * Determine whether this settings patch will trigger a wp-config.php sync.
     *
     * @param array<string,mixed> $sanitized
     * @param array<string,mixed> $old_settings
     */
    private function patch_requires_wp_config_write( array $sanitized, array $old_settings ): bool {
        if ( ! array_key_exists( 'disable_file_editor', $sanitized ) ) {
            return false;
        }

        return ( $old_settings['disable_file_editor'] ?? null ) !== $sanitized['disable_file_editor'];
    }

    /**
     * Whether wp-config.php can be updated directly without filesystem credentials.
     *
     * Returns true for non-permission blockers so callers can surface the real
     * hardening state instead of prompting for credentials that will not help.
     */
    public function can_update_wp_config_without_credentials(): bool {
        $status = \SiteFort\Modules\Tools\ConfigFilePolicy::get_wp_config_status();
        $path   = is_string( $status['path'] ?? null ) ? (string) $status['path'] : '';

        if ( empty( $status['write_enabled'] ) || $path === '' || ! file_exists( $path ) ) {
            return true;
        }

        return ! empty( $status['writable'] ) || FilesystemMutation::has_configured_write_transport();
    }

    public function get_php_block_snippets( WP_REST_Request $request ): WP_REST_Response {
        return new WP_REST_Response( [
            'success' => true,
            'data'    => $this->hardening->get_block_php_snippets(),
        ], 200 );
    }

    public function get_config_rules( WP_REST_Request $request ): WP_REST_Response {
        $compact = rest_sanitize_boolean( $request->get_param( 'compact' ) );
        $data = array_merge(
            $this->hardening->get_manual_config_rules( ! $compact ),
            [
                'server_rule_state'    => \SiteFort\Includes\ServerConfigWriter::get_state(),
                'wp_config_rule_state' => $this->hardening->get_wp_config_rule_state(),
            ]
        );

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

    public function get_server_rule_state( WP_REST_Request $request ): WP_REST_Response {
        return new WP_REST_Response( [
            'success' => true,
            'data'    => \SiteFort\Includes\ServerConfigWriter::get_state(),
        ], 200 );
    }

    public function get_wp_config_rule_state( WP_REST_Request $request ): WP_REST_Response {
        return new WP_REST_Response( [
            'success' => true,
            'data'    => $this->hardening->get_wp_config_rule_state(),
        ], 200 );
    }

    public function verify_server_rule_state( WP_REST_Request $request ): WP_REST_Response {
        $current_state = \SiteFort\Includes\ServerConfigWriter::get_state();
        $verification  = is_array( $current_state['verification'] ?? null ) ? (array) $current_state['verification'] : [];

        \SiteFort\Includes\ServerConfigWriter::run_verification(
            null,
            isset( $current_state['desired_rules_present'] ) ? (bool) $current_state['desired_rules_present'] : null,
            isset( $current_state['persisted'] ) ? (bool) $current_state['persisted'] : null,
            is_string( $current_state['server'] ?? null ) ? (string) $current_state['server'] : null,
            null,
            is_string( $verification['sync_hash'] ?? null ) ? (string) $verification['sync_hash'] : null
        );
        $this->hardening->clear_scheduled_server_rule_verification();

        return new WP_REST_Response( [
            'success' => true,
            'data'    => \SiteFort\Includes\ServerConfigWriter::get_state(),
        ], 200 );
    }

    public function get_file_permissions( WP_REST_Request $request ): WP_REST_Response {
        return new WP_REST_Response( [
            'success' => true,
            'data'    => $this->hardening->check_file_permissions(),
        ], 200 );
    }

    /**
     * Discover all /wp/v2/ REST API endpoints and classify their restriction status.
     */
    public function get_rest_api_endpoints( WP_REST_Request $request ): WP_REST_Response {
        $server = rest_get_server();
        $routes = $server->get_routes();

        $settings       = $this->hardening->get_settings();
        $allowed_routes = $settings['restrict_rest_api_allowed_routes'] ?? [];

        // Capability map matching WordPressTweaks::filter_rest_dispatch_request().
        $endpoint_caps = \SiteFort\Modules\Hardening\Tweaks\WordPressTweaks::get_restricted_rest_endpoint_capabilities();

        // Always-allowed segments (bypassed by the filter).
        $always_allowed = [ 'settings' ];

        $seen      = [];
        $endpoints = [];
        $post_type_map = null;
        $taxonomy_map  = null;
        $get_post_type_map = static function () use ( &$post_type_map ): array {
            if ( is_array( $post_type_map ) ) {
                return $post_type_map;
            }

            $post_type_map = [];
            foreach ( get_post_types( [], 'objects' ) as $post_type ) {
                $label = isset( $post_type->labels->name ) ? (string) $post_type->labels->name : (string) $post_type->name;
                $cap   = isset( $post_type->cap->edit_posts ) ? (string) $post_type->cap->edit_posts : null;
                $keys  = array_filter( [
                    isset( $post_type->rest_base ) ? (string) $post_type->rest_base : '',
                    isset( $post_type->name ) ? (string) $post_type->name : '',
                ] );

                foreach ( array_unique( $keys ) as $key ) {
                    $post_type_map[ $key ] = [
                        'label'      => $label,
                        'capability' => $cap,
                    ];
                }
            }

            return $post_type_map;
        };
        $get_taxonomy_map = static function () use ( &$taxonomy_map ): array {
            if ( is_array( $taxonomy_map ) ) {
                return $taxonomy_map;
            }

            $taxonomy_map = [];
            foreach ( get_taxonomies( [], 'objects' ) as $taxonomy ) {
                $label = isset( $taxonomy->labels->name ) ? (string) $taxonomy->labels->name : (string) $taxonomy->name;
                $cap   = isset( $taxonomy->cap->edit_terms ) ? (string) $taxonomy->cap->edit_terms : 'assign_terms';
                $keys  = array_filter( [
                    isset( $taxonomy->rest_base ) ? (string) $taxonomy->rest_base : '',
                    isset( $taxonomy->name ) ? (string) $taxonomy->name : '',
                ] );

                foreach ( array_unique( $keys ) as $key ) {
                    $taxonomy_map[ $key ] = [
                        'label'      => $label,
                        'capability' => $cap,
                    ];
                }
            }

            return $taxonomy_map;
        };

        foreach ( $routes as $route => $handlers ) {
            // Only /wp/v2/ routes.
            if ( 0 !== strpos( $route, '/wp/v2/' ) ) {
                continue;
            }

            $clean  = strtolower( trim( $route, '/' ) );
            $parts  = explode( '/', $clean );
            // parts[0] = wp, parts[1] = v2, parts[2] = segment
            $segment = $parts[2] ?? '';
            if ( '' === $segment || isset( $seen[ $segment ] ) ) {
                continue;
            }
            $seen[ $segment ] = true;

            // Determine status.
            $status     = 'unrestricted';
            $capability = null;
            $label      = ucfirst( str_replace( [ '-', '_' ], ' ', $segment ) );

            // Always allowed (has its own strong checks).
            if ( in_array( $segment, $always_allowed, true ) ) {
                $status     = 'always_allowed';
                $capability = \SiteFort\Includes\Capabilities::admin_capability();
            }
            // User-allowed via settings.
            elseif ( in_array( $segment, $allowed_routes, true ) ) {
                $status = 'user_allowed';
            }
            // Known endpoint caps.
            elseif ( isset( $endpoint_caps[ $segment ] ) ) {
                $status     = 'capability_restricted';
                $capability = $endpoint_caps[ $segment ];
            }
            // Taxonomies collection.
            elseif ( 'taxonomies' === $segment ) {
                $status     = 'capability_restricted';
                $capability = 'assign_terms';
            }
            else {
                // Check handler callbacks for Posts/Terms controllers.
                $found_controller = false;
                foreach ( $handlers as $handler ) {
                    if ( ! is_array( $handler['callback'] ?? null )
                        || ! isset( $handler['callback'][0] )
                        || ! is_object( $handler['callback'][0] )
                    ) {
                        continue;
                    }
                    $controller = $handler['callback'][0];

                    if ( \SiteFort\Modules\Hardening\Tweaks\WordPressTweaks::should_defer_rest_controller_permission_check( $controller ) ) {
                        $found_controller = true;
                        $status           = 'controller_protected';
                        $post_type_info   = $get_post_type_map()[ $segment ] ?? null;
                        if ( is_array( $post_type_info ) && isset( $post_type_info['label'] ) ) {
                            $label = (string) $post_type_info['label'];
                        }

                        break;
                    }

                    if ( $controller instanceof \WP_REST_Terms_Controller ) {
                        $found_controller = true;
                        $status = 'capability_restricted';
                        $taxonomy_info = $get_taxonomy_map()[ $segment ] ?? null;
                        if ( is_array( $taxonomy_info ) ) {
                            $capability = isset( $taxonomy_info['capability'] ) ? (string) $taxonomy_info['capability'] : $capability;
                            $label      = isset( $taxonomy_info['label'] ) ? (string) $taxonomy_info['label'] : $label;
                        }
                        break;
                    }

                    if ( $controller instanceof \WP_REST_Posts_Controller ) {
                        $found_controller = true;
                        $status = 'capability_restricted';
                        $post_type_info = $get_post_type_map()[ $segment ] ?? null;
                        if ( is_array( $post_type_info ) ) {
                            $capability = isset( $post_type_info['capability'] ) ? (string) $post_type_info['capability'] : $capability;
                            $label      = isset( $post_type_info['label'] ) ? (string) $post_type_info['label'] : $label;
                        }
                        break;
                    }
                }

                // Unknown controller: login-required for unauthenticated users.
                if ( ! $found_controller ) {
                    $status = 'login_required';
                }
            }

            $endpoints[] = [
                'segment'    => $segment,
                'status'     => $status,
                'capability' => $capability,
                'label'      => $label,
            ];
        }

        // Sort: capability_restricted first, then controller_protected, login_required,
        // user_allowed, always_allowed, unrestricted.
        $order = [
            'capability_restricted' => 0,
            'controller_protected'  => 1,
            'login_required'        => 2,
            'user_allowed'          => 3,
            'always_allowed'        => 4,
            'unrestricted'          => 5,
        ];
        usort( $endpoints, function ( $a, $b ) use ( $order ) {
            $oa = $order[ $a['status'] ] ?? 5;
            $ob = $order[ $b['status'] ] ?? 5;
            if ( $oa !== $ob ) {
                return $oa - $ob;
            }
            return strcmp( $a['segment'], $b['segment'] );
        } );

        return new WP_REST_Response( [ 'endpoints' => $endpoints ], 200 );
    }

    /**
     * Return live login lockouts and recent authentication security activity.
     */
    public function get_login_protection_overview( WP_REST_Request $request ): WP_REST_Response {
        $active_lockouts    = LockoutRegistry::get_active_lockouts( 50 );
        $allowlisted_lookup = $this->get_firewall_allowlist_lookup( $active_lockouts['ips'] );
        $settings           = $this->hardening->get_settings();
        $lockout_duration   = absint( $settings['limit_login_attempts_lockout'] ?? 1800 );
        $auth_activity      = AuditLog::get_formatted_entries( [
            'category' => 'authentication',
            'events'   => [
                'Login Failed',
                'IP Lockout',
                'User Lockout',
                'IP Lockout Cleared',
                'User Lockout Cleared',
            ],
            'summary_context' => 'login_protection',
            'limit'    => 15,
        ] );
        $auth_activity = array_map( [ $this, 'build_login_protection_activity_item' ], $auth_activity );

        $now = time();
        $ips = array_map(
            static function ( array $entry ) use ( $allowlisted_lookup, $lockout_duration, $now ): array {
                $ip        = isset( $entry['ip'] ) ? (string) $entry['ip'] : '';
                $locked_at = isset( $entry['locked_at'] ) ? (int) $entry['locked_at'] : 0;
                $expires   = $locked_at + $lockout_duration;

                return [
                    'ip'          => $ip,
                    'locked_at'   => $locked_at,
                    'expires_in'  => max( 0, $expires - $now ),
                    'allowlisted' => '' !== $ip && ! empty( $allowlisted_lookup[ $ip ] ),
                ];
            },
            $active_lockouts['ips']
        );

        $users = array_map(
            static function ( array $entry ) use ( $lockout_duration, $now ): array {
                $locked_at = isset( $entry['locked_at'] ) ? (int) $entry['locked_at'] : 0;
                $expires   = $locked_at + $lockout_duration;

                return [
                    'username'   => isset( $entry['username'] ) ? (string) $entry['username'] : '',
                    'locked_at'  => $locked_at,
                    'expires_in' => max( 0, $expires - $now ),
                ];
            },
            $active_lockouts['users']
        );

        return new WP_REST_Response(
            [
                'active_lockouts' => [
                    'ips'    => $ips,
                    'users'  => $users,
                    'counts' => [
                        'ip'    => count( $ips ),
                        'user'  => count( $users ),
                        'total' => count( $ips ) + count( $users ),
                    ],
                ],
                'auth_activity'   => $auth_activity,
            ],
            200
        );
    }

    /**
     * Trim generic audit-log entries to the fields rendered by the login protection widget.
     *
     * @param array<string,mixed> $entry
     * @return array{id:int,event_time:int,event:string,ip:string,severity:string,summary:string}
     */
    private function build_login_protection_activity_item( array $entry ): array {
        return [
            'id'         => isset( $entry['id'] ) ? (int) $entry['id'] : 0,
            'event_time' => isset( $entry['event_time'] ) ? (int) $entry['event_time'] : 0,
            'event'      => isset( $entry['event'] ) ? (string) $entry['event'] : '',
            'ip'         => isset( $entry['ip'] ) ? (string) $entry['ip'] : '',
            'severity'   => isset( $entry['severity'] ) ? (string) $entry['severity'] : 'info',
            'summary'    => isset( $entry['summary'] ) ? (string) $entry['summary'] : '',
        ];
    }

    /**
     * POST /hardening/lockout/unlock-ip
     * Delete the transient that enforces a temporary IP login lockout.
     */
    public function unlock_ip( WP_REST_Request $request ): WP_REST_Response {
        $params = $request->get_json_params();
        $ip     = sanitize_text_field( trim( $params['ip'] ?? '' ) );

        if ( '' === $ip ) {
            return new WP_REST_Response( [ 'error' => 'IP is required.' ], 400 );
        }

        Hardening::clear_ip_lockout( $ip );
        Hardening::clear_ip_login_attempts( $ip );
        do_action( 'sitefort_lockout_cleared', $ip, 'ip' );
        AuditLog::add_log(
            'IP Lockout Cleared',
            [
                'ip'             => $ip,
                'attempts_reset' => true,
            ],
            'warning',
            'authentication'
        );

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

    /**
     * POST /hardening/lockout/unlock-user
     * Delete the transient that enforces a temporary username login lockout.
     */
    public function unlock_user( WP_REST_Request $request ): WP_REST_Response {
        $params   = $request->get_json_params();
        $username = Hardening::sanitize_login_identifier_input( $params['username'] ?? '' );

        if ( '' === $username ) {
            return new WP_REST_Response( [ 'error' => 'Username is required.' ], 400 );
        }

        Hardening::clear_user_lockout( $username );
        Hardening::clear_user_login_attempts( $username );
        do_action( 'sitefort_lockout_cleared', $username, 'user' );
        AuditLog::add_log(
            'User Lockout Cleared',
            [
                'username'       => $username,
                'attempts_reset' => true,
            ],
            'warning',
            'authentication'
        );

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

    public function unlock_all(): WP_REST_Response {
        $counts = LockoutRegistry::count_active_lockouts();
        $total  = ( $counts['ip'] ?? 0 ) + ( $counts['user'] ?? 0 );

        LockoutRegistry::clear_all_lockouts();

        AuditLog::add_log(
            'All Lockouts Cleared',
            [ 'cleared' => $total ],
            'warning',
            'authentication'
        );

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

    /**
     * Build a lookup of IPs already present in the firewall allowlist.
     *
     * @return array<string,bool>
     */
    private function get_firewall_allowlist_lookup( array $active_ips ): array {
        $settings    = SettingsStore::get( SettingsStore::COL_FIREWALL );
        $ip_blocking = new IpBlocking( is_array( $settings ) ? $settings : [] );
        $lookup      = [];

        foreach ( $active_ips as $entry ) {
            if ( ! is_array( $entry ) || empty( $entry['ip'] ) ) {
                continue;
            }

            $ip = (string) $entry['ip'];
            $lookup[ $ip ] = $ip_blocking->is_allowed( $ip );
        }

        return $lookup;
    }
}
