<?php

declare(strict_types=1);

namespace SecurWP\Modules\Hardening;

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

use WP_REST_Server;
use WP_REST_Request;
use WP_REST_Response;
use WP_Error;
use SecurWP\Includes\SettingsStore;
use SecurWP\Modules\AuditLog\AuditLog;
use SecurWP\Modules\Firewall\IpBlocking;

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

    private Hardening $hardening;

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

    public function register_routes(): void {
        $ns = 'securwp/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/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 \SecurWP\Includes\Capabilities::can_manage_plugin();
    }

    /**
     * Build REST-safe hardening settings payload.
     */
    private function build_response_settings(): array {
        $settings = $this->hardening->get_settings();
        if ( ! is_array( $settings ) ) {
            $settings = [];
        }
        $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 );
        $settings['_permalink_structure'] = get_option( 'permalink_structure' );
        $settings['_server_type'] = \SecurWP\Includes\ServerConfigWriter::detect_server();
        $settings['_server_rule_state'] = \SecurWP\Includes\ServerConfigWriter::get_state();
        $settings['_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();
        return new WP_REST_Response( $settings, 200 );
    }

    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 );
        }

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

        // 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".', 'securwp' ),
            ], 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.', 'securwp' ),
                    $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.', 'securwp' )
            : __( 'Settings are already up to date.', 'securwp' );

        if ( $updated && $server_config_changed ) {
            $server_rule_state = \SecurWP\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;
            }
        }

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

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

        return new WP_REST_Response( $response_data, 200 );
    }

    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 {
        return new WP_REST_Response( [
            'success' => true,
            'data'    => array_merge(
                $this->hardening->get_manual_config_rules(),
                [
                    'server_rule_state'    => \SecurWP\Includes\ServerConfigWriter::get_state(),
                    'wp_config_rule_state' => $this->hardening->get_wp_config_rule_state(),
                ]
            ),
        ], 200 );
    }

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

    public function verify_server_rule_state( WP_REST_Request $request ): WP_REST_Response {
        \SecurWP\Includes\ServerConfigWriter::run_verification();
        $this->hardening->clear_scheduled_server_rule_verification();

        return new WP_REST_Response( [
            'success' => true,
            'data'    => \SecurWP\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 = \SecurWP\Modules\Hardening\Tweaks\WordPressTweaks::get_restricted_rest_endpoint_capabilities();

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

        $seen      = [];
        $endpoints = [];

        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 = \SecurWP\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 ( \SecurWP\Modules\Hardening\Tweaks\WordPressTweaks::should_defer_rest_controller_permission_check( $controller ) ) {
                        $found_controller = true;
                        $status           = 'controller_protected';

                        foreach ( get_post_types( [], 'objects' ) as $post_type ) {
                            if ( ( $post_type->rest_base ?? '' ) === $segment || $post_type->name === $segment ) {
                                $label = $post_type->labels->name ?? $label;
                                break;
                            }
                        }

                        break;
                    }

                    if ( $controller instanceof \WP_REST_Terms_Controller ) {
                        $found_controller = true;
                        $status = 'capability_restricted';
                        foreach ( get_taxonomies( [], 'objects' ) as $taxonomy ) {
                            if ( ( $taxonomy->rest_base ?? '' ) === $segment || $taxonomy->name === $segment ) {
                                $capability = $taxonomy->cap->edit_terms;
                                $label      = $taxonomy->labels->name ?? $label;
                                break;
                            }
                        }
                        break;
                    }

                    if ( $controller instanceof \WP_REST_Posts_Controller ) {
                        $found_controller = true;
                        $status = 'capability_restricted';
                        foreach ( get_post_types( [], 'objects' ) as $post_type ) {
                            if ( ( $post_type->rest_base ?? '' ) === $segment || $post_type->name === $segment ) {
                                $capability = $post_type->cap->edit_posts;
                                $label      = $post_type->labels->name ?? $label;
                                break;
                            }
                        }
                        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,
        ] );

        $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
        );
    }

    /**
     * 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( 'securwp_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( 'securwp_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 );
        $lookup   = [];

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

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

        return $lookup;
    }
}
