<?php
/**
 * LoginLimiterModule — Brute-force protection, lockouts, IP management, sessions.
 *
 * Responsibilities:
 *   • 7 DB tables  : login_attempts, login_limiter_lockouts, user_sessions,
 *                    user_activity, two_factor_auth, ll_blocked_ips, ll_allowed_ips
 *   • 5 admin pages: Dashboard, Login Attempts, IP Management, Lockouts, Settings
 *   • AJAX handlers: via LoginLimiterAjax (direct wp_ajax hooks, not AjaxManager)
 *   • Login hooks  : wp_login_failed, wp_login, wp_logout, wp_authenticate (early block)
 *   • Cron         : log pruning, expired-lockout cleanup
 *
 * Architecture rules:
 *   • This is a FREE module — requires_pro() returns false.
 *   • Self-contained: owns ll_blocked_ips / ll_allowed_ips tables instead of
 *     referencing the Firewall module's ip_blacklist / ip_whitelist tables.
 *   • All brute-force logic delegated to LoginLimiterEngine.
 *   • Cross-module data uses do_action / apply_filters only.
 *
 * @package UltraguardPro\modules\loginlimiter
 * @since   6.5.0
 */

namespace UltraguardPro\modules\loginlimiter;

use UltraguardPro\modules\core\AbstractModule;
use UltraguardPro\core\Utils;
use WP_Error;

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

class LoginLimiterModule extends AbstractModule {

    // ── Identity & license ─────────────────────────────────────────────────────

    public function get_id(): string { return 'login_limiter'; }

    /**
     * Register our settings prefix immediately so Settings::get_option() can
     * resolve 'loginlimiter_*' keys before boot() is called.
     */
    public function __construct( \UltraguardPro\Plugin $plugin ) {
        parent::__construct( $plugin );
        $this->plugin->settings->register_module_prefix( 'loginlimiter_', 'loginlimiter' );
        $this->plugin->settings->register_module_prefix( 'login_protect_', 'login_protect' );
    }

    /** Free module — available to all users. */
    protected function requires_pro(): bool { return false; }

    // ── Boot ──────────────────────────────────────────────────────────────────

    /**
     * Bootstrap: schema → AJAX → contributor → pages → runtime hooks.
     */
    public function boot(): void {
        // 1. DB schema (7 tables)
        $_db_tables = $this->plugin->modules->get_db_tables();
        if ( $_db_tables ) {
            $_db_tables->register_schema( new schema\LoginLimiterSchema() );
        }

        if ( method_exists( $this->plugin->settings, 'register_settings_schema' ) ) {
            $this->plugin->settings->register_settings_schema( 'login_protect', 'login_protect_', [
                'login_protect_enabled'              => [ 'type' => 'bool', 'default' => true ],
                'login_protect_custom_path'          => [ 'type' => 'text', 'default' => '' ],
                'login_protect_original_action'      => [ 'type' => 'select', 'default' => '404', 'options' => [ '404', 'redirect_home', 'redirect_custom' ] ],
                'login_protect_custom_redirect_url'  => [ 'type' => 'text', 'default' => '' ],
                'login_protect_log_attempts'         => [ 'type' => 'bool', 'default' => true ],
                'login_protect_auto_block'           => [ 'type' => 'bool', 'default' => false ],
                'login_protect_auto_block_threshold' => [ 'type' => 'int', 'default' => 5 ],
                'login_protect_notify_admin'         => [ 'type' => 'bool', 'default' => false ],
            ] );
        }

        // 2. AJAX handlers
        ( new ajax\LoginLimiterAjax( $this->plugin ) )->register();

        // 3. Dashboard contributor
        ( new LoginLimiterDashboardContributor( $this->plugin ) )->register();

        // 4. Admin pages
        add_action( 'ultraguard_register_admin_pages', [ $this, 'register_pages' ], 15, 1 );

        // 5. Early login-page block (fires on login_init before the form renders)
        add_action( 'login_init', [ $this, 'maybe_block_request' ], 1 );
        add_action( 'login_form', [ $this, 'inject_captcha_widget' ], 30 );
        add_action( 'login_enqueue_scripts', [ $this, 'enqueue_captcha_scripts' ] );
        add_filter( 'authenticate', [ $this, 'validate_captcha_login' ], 20, 3 );

        // 6. Login event hooks
        add_action( 'wp_login_failed',  [ $this, 'on_login_failed' ], 10, 2 );
        add_action( 'wp_login',         [ $this, 'on_login_success' ], 10, 2 );
        add_action( 'wp_logout',        [ $this, 'on_logout' ] );
        add_action( 'ultraguard_pro/login_protect/old_url_hit', [ $this, 'on_protected_login_url_hit' ], 10, 1 );

        // 7. Hide login error details when configured
        add_filter( 'login_errors',     [ $this, 'maybe_hide_errors' ] );
        add_filter( 'login_url',        [ $this, 'maybe_rename_login_url' ], 10, 3 );

        // 8. Cron hooks
        add_action( 'ultraguard_loginlimiter_cleanup',  [ $this, 'run_cleanup' ] );
        add_action( 'init',                             [ $this, 'maybe_schedule_cron' ], 30 );
        add_action( 'ultraguard_after_enqueue_assets',  [ $this, 'enqueue_assets' ] );
        add_filter( 'ultraguard_pro/compliance/available_sections', [ $this, 'add_compliance_sections' ] );
        add_filter( 'ultraguard_pro/compliance/collect_data', [ $this, 'add_compliance_data' ], 10, 4 );

        do_action( 'ultraguard_pro/login_limiter/booted', $this );
    }

    // ── Admin pages ────────────────────────────────────────────────────────────

    /**
     * Register all five admin pages.
     *
     * @param object $admin  AdminManager instance.
     */
    public function register_pages( $admin ): void {

        // Parent page — Login Protection dashboard
        $admin->register_page( [
            'id'              => 'login-limiter',
            'page_title'      => __( 'Login Protection', 'ultraguard-security-suite' ),
            'menu_title'      => __( 'Login Limiter',   'ultraguard-security-suite' ),
            'capability'      => 'manage_options',
            'scheme_callback' => [ pages\LoginLimiterDashboard::class, $this->plugin ],
            'is_parent'       => true,
            'priority'        => 15,
            'group'           => 'Access Control',
            'icon'            => 'dashicons-lock',
        ] );

        $subpages = [
            [
                'id'    => 'login-attempts',
                'title' => __( 'Login Attempts',        'ultraguard-security-suite' ),
                'menu'  => __( 'Login Attempts',        'ultraguard-security-suite' ),
                'class' => pages\LoginAttemptsPage::class,
                'p'     => 16,
                'icon'  => 'dashicons-chart-bar',
            ],
            [
                'id'    => 'ip-management',
                'title' => __( 'IP Management',         'ultraguard-security-suite' ),
                'menu'  => __( 'IP Management',         'ultraguard-security-suite' ),
                'class' => pages\IpManagementPage::class,
                'p'     => 17,
                'icon'  => 'dashicons-networking',
            ],
            [
                'id'    => 'lockouts',
                'title' => __( 'Lockouts',              'ultraguard-security-suite' ),
                'menu'  => __( 'Lockouts',              'ultraguard-security-suite' ),
                'class' => pages\LockoutsPage::class,
                'p'     => 18,
                'icon'  => 'dashicons-dismiss',
            ],
            [
                'id'    => 'login-limiter-settings',
                'title' => __( 'Login Limiter Settings','ultraguard-security-suite' ),
                'menu'  => __( 'Settings',              'ultraguard-security-suite' ),
                'class' => pages\LoginLimiterSettingsPage::class,
                'p'     => 19,
                'icon'  => 'dashicons-admin-settings',
            ],
        ];

        foreach ( $subpages as $p ) {
            $admin->register_page( [
                'id'              => $p['id'],
                'page_title'      => $p['title'],
                'menu_title'      => $p['menu'],
                'capability'      => 'manage_options',
                'scheme_callback' => [ $p['class'], $this->plugin ],
                'parent_id'       => 'login-limiter',
                'priority'        => $p['p'],
                'group'           => 'Access Control',
                'icon'            => $p['icon'],
            ] );
        }
    }

    public function add_compliance_sections( array $sections ): array {
        $sections['authentication_events'] = __( 'Authentication Events', 'ultraguard-security-suite' );
        return $sections;
    }

    public function add_compliance_data( array $sections, string $type, array $options, array $meta ): array {
        $requested = isset( $options['sections'] ) && is_array( $options['sections'] ) ? $options['sections'] : [];
        if ( $requested && ! in_array( 'authentication_events', $requested, true ) ) {
            return $sections;
        }

        $attempts_table = $this->get_table( 'login_attempts' );
        $lockouts_table = $this->get_table( 'login_limiter_lockouts' );

        if ( ! $attempts_table ) {
            return $sections;
        }

        $summary = method_exists( $attempts_table, 'get_attempt_summary' ) ? $attempts_table->get_attempt_summary() : [
            'total'        => (int) $attempts_table->count(),
            'failed_24h'   => (int) $attempts_table->count( [ 'status' => 'failed' ] ),
            'success'      => (int) $attempts_table->count( [ 'status' => 'success' ] ),
        ];
        $recent = method_exists( $attempts_table, 'get_recent' ) ? $attempts_table->get_recent( 15 ) : $attempts_table->get_by( [], 15, 'created_at', 'DESC' );

        $section = $sections['authentication_events'] ?? [
            'title'   => __( 'Authentication Events', 'ultraguard-security-suite' ),
            'content' => [],
        ];

        $content = is_array( $section['content'] ?? null ) ? $section['content'] : [];
        $content['login_limiter'] = [
            'failed_attempts_24h' => (int) ( $summary['failed_24h'] ?? 0 ),
            'successful_logins'   => (int) ( $summary['success'] ?? 0 ),
            'active_lockouts'     => $lockouts_table ? (int) $lockouts_table->count( [ 'is_active' => 1 ] ) : 0,
            'hidden_login_active' => $this->option_enabled( 'login_protect_enabled' ) && '' !== (string) $this->option( 'login_protect_custom_path', '' ),
        ];
        $content['recent_attempts'] = array_map( static function( array $row ): array {
            return [
                'ip_address' => $row['ip_address'] ?? '',
                'username'   => $row['username'] ?? '',
                'status'     => $row['status'] ?? '',
                'created_at' => $row['created_at'] ?? '',
            ];
        }, $recent );

        $section['content']               = $content;
        $sections['authentication_events'] = $section;

        return $sections;
    }

    // ── Login event handlers ───────────────────────────────────────────────────

    /**
     * Public alias called by Plugin::early_security_init() on the login page.
     * Also checks the cloud threat intelligence list.
     */
    public function maybe_block_locked_out_ip(): void {
        // Cloud threat-intelligence check (in addition to module-level lockouts)
        $ip       = \UltraguardPro\core\Utils::get_client_ip();
        $db_tables = $this->plugin->modules->get_db_tables();
        $threat_t  = $db_tables ? $db_tables->get_table( 'threat_ips' ) : null;
        if ( $threat_t && $threat_t->is_threat( $ip ) ) {
            // Allow admins through regardless
        if ( is_user_logged_in() && \UltraguardPro\ultraguard_current_user_can_manage() ) { return; }
            status_header( 403 );
            wp_die(
                esc_html__( 'Access denied. Your IP address has been flagged by UltraGuard threat intelligence.', 'ultraguard-security-suite' ),
                esc_html__( 'Access Denied', 'ultraguard-security-suite' ),
                [ 'response' => 403 ]
            );
        }
        $this->maybe_block_request();
    }

    /**
     * Fired early on login_init: block locked-out or banned IPs before the form renders.
     */
    public function maybe_block_request(): void {
        if ( ! $this->option( 'loginlimiter_enabled', true ) ) { return; }

        $ip      = Utils::get_client_ip();
        $lo_t    = $this->get_table( 'login_limiter_lockouts' );
        $block_t = $this->get_table( 'll_blocked_ips' );

        $is_locked = $lo_t    && $lo_t->is_locked_out( $ip );
        $is_banned = $block_t && $block_t->is_blocked( $ip );

        if ( ! $is_locked && ! $is_banned ) { return; }

        // Whitelisted IPs are never blocked
        $allow_t = $this->get_table( 'll_allowed_ips' );
        if ( $allow_t && $allow_t->is_allowed( $ip ) ) { return; }

        status_header( 403 );
        wp_die(
            esc_html( apply_filters(
                'ultraguard_lockout_message',
                __( 'Too many failed login attempts. Please try again later.', 'ultraguard-security-suite' )
            ) ),
            esc_html__( 'Access Denied', 'ultraguard-security-suite' ),
            [ 'response' => 403 ]
        );
    }

    /**
     * Record a failed login attempt and trigger lockout if threshold is reached.
     *
     * @param string         $username   The username attempted.
     * @param \WP_Error|null $error      WP_Error from the failed login.
     */
    public function on_login_failed( string $username, $error = null ): void {
        if ( ! $this->option( 'loginlimiter_enabled', true ) ) { return; }

        $ip = Utils::get_client_ip();

        // Whitelisted IPs are exempt from brute-force counting
        $allow_t = $this->get_table( 'll_allowed_ips' );
        if ( $allow_t && $allow_t->is_allowed( $ip ) ) { return; }

        $att_t = $this->get_table( 'login_attempts' );
        if ( $att_t ) {
            $att_t->log_attempt( $ip, $username, 'failed' );
        }

        $this->engine()->maybe_lockout( $ip, $username );
    }

    /**
     * Count requests to the protected legacy login URL as failed login attempts
     * so Login Protect and Login Limiter share the same lockout controls.
     *
     * @param array $event
     */
    public function on_protected_login_url_hit( array $event ): void {
        if ( ! $this->option( 'loginlimiter_enabled', true ) ) { return; }

        $ip = sanitize_text_field( (string) ( $event['ip_address'] ?? '' ) );
        if ( ! filter_var( $ip, FILTER_VALIDATE_IP ) ) { return; }

        $allow_t = $this->get_table( 'll_allowed_ips' );
        if ( $allow_t && $allow_t->is_allowed( $ip ) ) { return; }

        $attempts_t = $this->get_table( 'login_attempts' );
        if ( $attempts_t && method_exists( $attempts_t, 'insert' ) ) {
            $country_code = strtoupper( (string) Utils::get_country_from_ip( $ip ) );
            $attempts_t->insert(
                [
                    'ip_address'   => $ip,
                    'username'     => 'hidden-login',
                    'status'       => 'failed',
                    'user_agent'   => sanitize_text_field( substr( (string) ( $event['user_agent'] ?? '' ), 0, 500 ) ),
                    'country_code' => ( 'XX' !== $country_code && preg_match( '/^[A-Z]{2}$/', $country_code ) ) ? $country_code : null,
                    'created_at'   => current_time( 'mysql', true ),
                ]
            );
        }

        $this->engine()->maybe_lockout( $ip, 'hidden-login' );
    }

    /**
     * Record a successful login and clear existing lockouts for the IP.
     *
     * @param string   $user_login  The user's login name.
     * @param \WP_User $user        The logged-in user object.
     */
    public function on_login_success( string $user_login, \WP_User $user ): void {
        $ip = Utils::get_client_ip();

        if ( $att_t = $this->get_table( 'login_attempts' ) ) {
            $att_t->log_attempt( $ip, $user_login, 'success' );
        }

        if ( $this->option( 'loginlimiter_track_activity', true ) ) {
            if ( $act_t = $this->get_table( 'user_activity' ) ) {
                $act_t->record( $user->ID, 'login', sprintf( 'Logged in from %s', $ip ) );
            }
        }

        // Auto-release any lockout on successful auth (optional — could be a setting)
        if ( $this->option( 'loginlimiter_clear_lockout_on_success', false ) ) {
            if ( $lo_t = $this->get_table( 'login_limiter_lockouts' ) ) {
                $lo_t->release( $ip );
            }
        }

        do_action( 'ultraguard_login_success', $ip, $user );
    }

    /**
     * Record a logout event.
     */
    public function on_logout(): void {
        $user_id = get_current_user_id();
        if ( ! $user_id ) { return; }

        if ( $this->option( 'loginlimiter_track_activity', true ) ) {
            if ( $act_t = $this->get_table( 'user_activity' ) ) {
                $act_t->record( $user_id, 'logout', 'Logged out' );
            }
        }

        do_action( 'ultraguard_logout', $user_id );
    }

    /**
     * Optionally replace specific login errors with a generic message.
     *
     * @param  string $error_html  The default error HTML from WordPress.
     * @return string
     */
    public function maybe_hide_errors( string $error_html ): string {
        if ( $this->option( 'loginlimiter_hide_errors', true ) ) {
            return '<strong>' . esc_html__( 'Error:', 'ultraguard-security-suite' ) . '</strong> '
                 . esc_html__( 'Invalid credentials. Please try again.', 'ultraguard-security-suite' );
        }
        return $error_html;
    }

    /**
     * Optional custom login URL slug (rename /wp-login.php).
     *
     * @param  string $login_url
     * @param  string $redirect
     * @param  bool   $force_reauth
     * @return string
     */
    public function maybe_rename_login_url( string $login_url, string $redirect, bool $force_reauth ): string {
        $custom_slug = $this->option( 'loginlimiter_custom_login_slug', '' );
        if ( ! $custom_slug ) { return $login_url; }

        $args = $redirect      ? [ 'redirect_to' => $redirect ]  : [];
        $args = $force_reauth  ? array_merge( $args, [ 'reauth' => '1' ] ) : $args;

        return add_query_arg( $args, home_url( '/' . ltrim( $custom_slug, '/' ) ) );
    }

    /**
     * Inject the CAPTCHA widget on the login form when the current IP requires it.
     */
    public function inject_captcha_widget(): void {
        $captcha = $this->captcha_engine();

        if ( $captcha->should_show( Utils::get_client_ip() ) ) {
            $captcha->inject_login_form_widget();
        }
    }

    /**
     * Enqueue CAPTCHA provider scripts on the login screen when needed.
     */
    public function enqueue_captcha_scripts(): void {
        $captcha = $this->captcha_engine();

        if ( $captcha->should_show( Utils::get_client_ip() ) ) {
            $captcha->enqueue_scripts();
        }
    }

    /**
     * Enforce CAPTCHA verification on the login flow when the challenge is required.
     *
     * @param WP_User|WP_Error|null $user
     * @param string                $username
     * @param string                $password
     * @return WP_User|WP_Error|null
     */
    public function validate_captcha_login( $user, string $username, string $password ) {
        $captcha = $this->captcha_engine();
        $ip      = Utils::get_client_ip();

        if ( ! $captcha->should_show( $ip ) ) {
            return $user;
        }

        if ( is_wp_error( $user ) ) {
            return $user;
        }

        $provider = $captcha->get_provider();
        $token    = '';

        if ( CaptchaEngine::PROVIDER_TURNSTILE === $provider ) {
            $token = sanitize_text_field( wp_unslash( $_POST['cf-turnstile-response'] ?? '' ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- CAPTCHA token is verified server-side during the login request flow.
        } else {
            $token = sanitize_text_field( wp_unslash( $_POST['g-recaptcha-response'] ?? '' ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- CAPTCHA token is verified server-side during the login request flow.
        }

        if ( ! $captcha->verify( $token ) ) {
            return new WP_Error(
                'ultraguard_captcha_failed',
                __( '<strong>Error:</strong> CAPTCHA verification failed. Please try again.', 'ultraguard-security-suite' )
            );
        }

        return $user;
    }

    // ── Cron ──────────────────────────────────────────────────────────────────

    /**
     * Schedule the daily cleanup event on init if not already scheduled.
     */
    public function maybe_schedule_cron(): void {
        if ( ! wp_next_scheduled( 'ultraguard_loginlimiter_cleanup' ) ) {
            wp_schedule_event( time() + 3600, 'daily', 'ultraguard_loginlimiter_cleanup' );
        }
    }

    /**
     * Cleanup: expire old lockouts + prune log entries older than retention period.
     */
    public function run_cleanup(): void {
        if ( $lo_t = $this->get_table( 'login_limiter_lockouts' ) ) {
            $lo_t->cleanup_expired();
        }

        $retention_days = (int) $this->option( 'loginlimiter_log_retention', 30 );
        if ( $att_t = $this->get_table( 'login_attempts' ) ) {
            $att_t->delete_older_than( $retention_days, 'created_at' );
        }

        if ( $act_t = $this->get_table( 'user_activity' ) ) {
            $act_t->delete_older_than( $retention_days * 3, 'created_at' );
        }
    }

    public function enqueue_assets(): void {
        $ug_page = sanitize_key( wp_unslash( $_GET['ug_page'] ?? ''  ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin screen detection.
        if ( ! in_array( $ug_page, [ 'login-limiter', 'login-attempts', 'ip-management', 'lockouts', 'login-limiter-settings' ], true ) ) {
            return;
        }
    }

    // ── Internal factory ──────────────────────────────────────────────────────

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

    /** Shortcut to the engine (for use in hooks). */
    private function engine(): LoginLimiterEngine {
        return $this->get_engine();
    }

    /**
     * Lazily create the CAPTCHA engine.
     */
    private function captcha_engine(): CaptchaEngine {
        static $engine = null;

        if ( null === $engine ) {
            $engine = new CaptchaEngine( $this->plugin );
        }

        return $engine;
    }
}
