<?php

declare(strict_types=1);

namespace SecurWP\Modules\Hardening\Tweaks;

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

use SecurWP\Includes\IpDetector;

/**
 * CAPTCHA / Bot Detection Provider.
 *
 * Supports Google reCAPTCHA v2 (checkbox and invisible), reCAPTCHA v3 (score-based),
 * and Cloudflare Turnstile for protecting the WordPress login form against
 * automated brute-force attacks.
 *
 */
class CaptchaProvider {

    /**
     * Hardening module settings.
     *
     * @var array
     */
    private array $settings;

    /**
     * @param array $settings The full hardening settings array.
     */
    public function __construct( array $settings ) {
        $this->settings = $settings;
    }

    /**
     * Whether the CAPTCHA integration is fully configured and enabled.
     *
     * Requires both a site key and a secret key to be present.
     *
     * @return bool
     */
    public function is_enabled(): bool {
        if ( empty( $this->settings['captcha_enabled'] ) ) {
            return false;
        }

        if ( empty( $this->settings['captcha_site_key'] ) || empty( $this->get_secret_key() ) ) {
            return false;
        }

        return true;
    }

    /**
     * Get the active CAPTCHA provider name.
     *
     * @return string 'recaptcha_v2_checkbox', 'recaptcha_v2_invisible', 'recaptcha_v3', or 'turnstile'.
     */
    public function get_provider(): string {
        $provider = (string) ( $this->settings['captcha_provider'] ?? 'recaptcha_v3' );

        if ( $provider === 'recaptcha_v2' ) {
            return 'recaptcha_v2_checkbox';
        }

        if ( ! in_array( $provider, [ 'recaptcha_v2_checkbox', 'recaptcha_v2_invisible', 'recaptcha_v3', 'turnstile' ], true ) ) {
            return 'recaptcha_v3';
        }

        return $provider;
    }

    /**
     * Verify a CAPTCHA token with the remote provider API.
     *
     * @param string $token The CAPTCHA response token from the client.
     * @param string $ip    The remote IP address of the user.
     * @return array{success: bool, score: float|null, action: string|null, error_codes: array}
     */
    public function verify( string $token, string $ip ): array {
        $secret = $this->get_secret_key();
        if ( $secret === '' ) {
            return [
                'success'     => false,
                'score'       => null,
                'action'      => null,
                'error_codes' => [ 'missing-secret' ],
            ];
        }

        if ( $this->get_provider() === 'turnstile' ) {
            $url = 'https://challenges.cloudflare.com/turnstile/v0/siteverify';
        } else {
            $url = 'https://www.google.com/recaptcha/api/siteverify';
        }

        $body = [
            'secret'   => $secret,
            'response' => $token,
            'remoteip' => $ip,
        ];

        $response = wp_remote_post( $url, [
            'body'      => $body,
            'timeout'   => 10,
            'sslverify' => true,
        ] );

        // Network / HTTP error.
        if ( is_wp_error( $response ) ) {
            return [
                'success'     => false,
                'score'       => null,
                'action'      => null,
                'error_codes' => [ $response->get_error_message() ],
            ];
        }

        $status_code = wp_remote_retrieve_response_code( $response );
        $body_raw    = wp_remote_retrieve_body( $response );
        $data        = json_decode( $body_raw, true );

        if ( $status_code !== 200 || ! is_array( $data ) ) {
            return [
                'success'     => false,
                'score'       => null,
                'action'      => null,
                'error_codes' => [ 'invalid-response' ],
            ];
        }

        $success     = ! empty( $data['success'] );
        $score       = isset( $data['score'] ) ? (float) $data['score'] : null;
        $action      = isset( $data['action'] ) ? (string) $data['action'] : null;
        $error_codes = isset( $data['error-codes'] ) && is_array( $data['error-codes'] )
            ? $data['error-codes']
            : [];

        return [
            'success'     => $success,
            'score'       => $score,
            'action'      => $action,
            'error_codes' => $error_codes,
        ];
    }

    /**
     * Resolve CAPTCHA secret from runtime settings or secure storage.
     */
    private function get_secret_key(): string {
        $inline = trim( (string) ( $this->settings['captcha_secret_key'] ?? '' ) );
        if ( $inline !== '' ) {
            return $inline;
        }
        if ( class_exists( '\\SecurWP\\Modules\\Hardening\\Hardening' ) ) {
            return \SecurWP\Modules\Hardening\Hardening::get_captcha_secret( $this->get_provider() );
        }
        return '';
    }

    /**
     * Determine if the score indicates a human visitor.
     *
     * Only applicable to reCAPTCHA v3 which returns a 0.0-1.0 score.
     *
     * @param float $score The score returned from reCAPTCHA v3.
     * @return bool
     */
    public function is_human( float $score ): bool {
        $threshold = (float) ( $this->settings['captcha_threshold'] ?? 0.5 );

        return $score >= $threshold;
    }

    /**
     * Enqueue the appropriate CAPTCHA JavaScript on the login page.
     *
     * Hooked to `login_enqueue_scripts`.
     *
     * @return void
     */
    public function enqueue_login_scripts(): void {
        $site_key = $this->settings['captcha_site_key'] ?? '';

        if ( empty( $site_key ) ) {
            return;
        }

        $this->enqueue_login_styles();

        $provider = $this->get_provider();

        if ( $provider === 'turnstile' ) {
            $this->enqueue_turnstile_scripts( $site_key );
        } elseif ( $provider === 'recaptcha_v2_checkbox' ) {
            $this->enqueue_recaptcha_v2_scripts( $site_key );
        } elseif ( $provider === 'recaptcha_v2_invisible' ) {
            $this->enqueue_recaptcha_v2_invisible_scripts( $site_key );
        } else {
            $this->enqueue_recaptcha_scripts( $site_key );
        }
    }

    /**
     * Render a hidden input field in the login form.
     *
     * Hooked to `login_form` action.
     *
     * @return void
     */
	public function render_captcha_field(): void {
		$provider = $this->get_provider();
		$site_key = (string) ( $this->settings['captcha_site_key'] ?? '' );

		if ( $provider === 'turnstile' ) {
			echo '<div class="securwp-login-captcha securwp-login-captcha--turnstile">';
			echo '<div class="cf-turnstile" data-sitekey="' . esc_attr( $site_key ) . '" data-callback="securwpTurnstileCallback"></div>';
			echo '</div>';
			$this->render_hidden_token_field();
		} elseif ( $provider === 'recaptcha_v2_checkbox' ) {
			echo '<div class="securwp-login-captcha securwp-login-captcha--recaptcha-v2">';
			echo '<div class="g-recaptcha" data-sitekey="' . esc_attr( $site_key ) . '" data-callback="securwpRecaptchaV2Callback"></div>';
			echo '</div>';
			$this->render_hidden_token_field();
		} elseif ( $provider === 'recaptcha_v2_invisible' ) {
			echo '<div id="securwp-recaptcha-v2-invisible" class="g-recaptcha" data-sitekey="' . esc_attr( $site_key ) . '" data-size="invisible" data-callback="securwpRecaptchaV2InvisibleCallback"></div>';
			$this->render_hidden_token_field();
		} else {
            // reCAPTCHA v3: the inline JS will populate this hidden field.
            $this->render_hidden_token_field();
        }
    }

    /**
     * Print the hidden token field used by all CAPTCHA providers.
     */
    private function render_hidden_token_field(): void {
        echo '<input type="hidden" name="securwp_captcha_token" id="securwp_captcha_token" value="" />';
    }

    /**
     * Validate the CAPTCHA token during authentication.
     *
     * Hooked to the `authenticate` filter at priority 20.
     *
     * @param \WP_User|\WP_Error|null $user     The authenticated user object, error, or null.
     * @param string                  $username  The submitted username.
     * @param string                  $password  The submitted password.
     * @return \WP_User|\WP_Error
     */
    public function check_login( $user, string $username, string $password ) {
        // If already an error, pass through (another filter already rejected).
        if ( is_wp_error( $user ) ) {
            return $user;
        }

        // If username or password is empty, WP hasn't attempted auth yet.
        if ( empty( $username ) || empty( $password ) ) {
            return $user;
        }

        // Bypass for XML-RPC requests.
        if ( defined( 'XMLRPC_REQUEST' ) && XMLRPC_REQUEST ) {
            return $user;
        }

	        // phpcs:ignore WordPress.Security.NonceVerification.Missing -- login CAPTCHA token is validated against the remote provider before authentication succeeds.
	        $token = isset( $_POST['securwp_captcha_token'] ) ? sanitize_text_field( wp_unslash( $_POST['securwp_captcha_token'] ) ) : '';

        if ( empty( $token ) ) {
            return new \WP_Error(
                'captcha_failed',
                __( 'CAPTCHA verification failed. Please try again.', 'securwp' )
            );
        }

        // Use the central IpDetector (per-request cached, handles proxies).
        $ip = IpDetector::get_ip();

        $result = $this->verify( $token, $ip );

        $provider = $this->get_provider();

        if ( in_array( $provider, [ 'turnstile', 'recaptcha_v2_checkbox', 'recaptcha_v2_invisible' ], true ) ) {
            // Turnstile and reCAPTCHA v2: simple success boolean check.
            if ( ! $result['success'] ) {
                return new \WP_Error(
                    'captcha_failed',
                    __( 'CAPTCHA verification failed.', 'securwp' )
                );
            }
        } else {
            // reCAPTCHA v3: must succeed AND meet score threshold AND match expected action.
            if ( ! $result['success'] ) {
                return new \WP_Error(
                    'captcha_failed',
                    __( 'CAPTCHA verification failed.', 'securwp' )
                );
            }

            // Verify the action field matches the expected action to prevent
            // token reuse from a different page/context.
            if ( $result['action'] !== null && $result['action'] !== 'login' ) {
                return new \WP_Error(
                    'captcha_failed',
                    __( 'CAPTCHA verification failed.', 'securwp' )
                );
            }

            if ( $result['score'] === null || ! $this->is_human( $result['score'] ) ) {
                return new \WP_Error(
                    'captcha_failed',
                    __( 'CAPTCHA verification failed.', 'securwp' )
                );
            }
        }

        return $user;
    }

    /**
     * Enqueue Google reCAPTCHA v3 scripts.
     *
     * @param string $site_key The reCAPTCHA v3 site key.
     * @return void
     */
	private function enqueue_recaptcha_scripts( string $site_key ): void {
		$escaped_key = esc_js( $site_key );

		// phpcs:ignore PluginCheck.CodeAnalysis.EnqueuedResourceOffloading.OffloadedContent -- Optional Google reCAPTCHA service integration requires Google's hosted client script.
		wp_enqueue_script(
			'securwp-recaptcha',
			'https://www.google.com/recaptcha/api.js?render=' . rawurlencode( $site_key ),
			[],
			SECURWP_VERSION,
			true
		);

		$inline_js = implode(
			"\n",
			[
				"document.addEventListener('DOMContentLoaded', function() {",
				"    var loginForm = document.getElementById('loginform');",
				"    if (!loginForm) return;",
				'',
				"    loginForm.addEventListener('submit', function(e) {",
				"        e.preventDefault();",
				"        var form = this;",
				"        grecaptcha.ready(function() {",
				"            grecaptcha.execute('{$escaped_key}', {action: 'login'}).then(function(token) {",
				"                var field = document.getElementById('securwp_captcha_token');",
				"                if (field) {",
				"                    field.value = token;",
				"                }",
				"                form.submit();",
				"            });",
				"        });",
				"    });",
				"});",
			]
		);

		wp_add_inline_script( 'securwp-recaptcha', $inline_js );
	}

    /**
     * Enqueue Google reCAPTCHA v2 (checkbox) scripts.
     *
     * @param string $site_key The reCAPTCHA v2 site key.
     * @return void
     */
	private function enqueue_recaptcha_v2_scripts( string $site_key ): void {
		// phpcs:ignore PluginCheck.CodeAnalysis.EnqueuedResourceOffloading.OffloadedContent -- Optional Google reCAPTCHA service integration requires Google's hosted client script.
		wp_enqueue_script(
			'securwp-recaptcha-v2',
			'https://www.google.com/recaptcha/api.js',
			[],
			SECURWP_VERSION,
			true
		);

        $inline_js = <<<'JS'
function securwpRecaptchaV2Callback(token) {
    var field = document.getElementById('securwp_captcha_token');
    if (field) {
        field.value = token;
    }
}
JS;

        wp_add_inline_script( 'securwp-recaptcha-v2', $inline_js );
    }

    /**
     * Enqueue Google reCAPTCHA v2 invisible scripts.
     *
     * @param string $site_key The reCAPTCHA v2 site key.
     * @return void
     */
	private function enqueue_recaptcha_v2_invisible_scripts( string $site_key ): void {
		$escaped_key = esc_js( $site_key );

		// phpcs:ignore PluginCheck.CodeAnalysis.EnqueuedResourceOffloading.OffloadedContent -- Optional Google reCAPTCHA service integration requires Google's hosted client script.
		wp_enqueue_script(
			'securwp-recaptcha-v2-invisible',
			'https://www.google.com/recaptcha/api.js?onload=securwpRecaptchaV2InvisibleOnload&render=explicit',
			[],
			SECURWP_VERSION,
			true
		);

		$inline_js = implode(
			"\n",
			[
				'window.securwpRecaptchaV2InvisibleWidget = null;',
				'window.securwpRecaptchaV2InvisiblePendingForm = null;',
				'',
				'function securwpRecaptchaV2InvisibleCallback(token) {',
				"    var field = document.getElementById('securwp_captcha_token');",
				'    if (field) {',
				'        field.value = token;',
				'    }',
				'',
				'    if (window.securwpRecaptchaV2InvisiblePendingForm) {',
				'        var form = window.securwpRecaptchaV2InvisiblePendingForm;',
				'        window.securwpRecaptchaV2InvisiblePendingForm = null;',
				'        form.submit();',
				'    }',
				'}',
				'',
				'function securwpRecaptchaV2InvisibleOnload() {',
				"    var container = document.getElementById('securwp-recaptcha-v2-invisible');",
				"    if (!container || typeof grecaptcha === 'undefined') {",
				'        return;',
				'    }',
				'',
				'    window.securwpRecaptchaV2InvisibleWidget = grecaptcha.render(container, {',
				"        sitekey: '{$escaped_key}',",
				"        size: 'invisible',",
				'        callback: securwpRecaptchaV2InvisibleCallback',
				'    });',
				'}',
				'',
				"document.addEventListener('DOMContentLoaded', function() {",
				"    var loginForm = document.getElementById('loginform');",
				'    if (!loginForm) return;',
				'',
				"    loginForm.addEventListener('submit', function(e) {",
				"        var field = document.getElementById('securwp_captcha_token');",
				'        if (field && field.value) {',
				'            return;',
				'        }',
				'',
				"        if (typeof grecaptcha === 'undefined' || window.securwpRecaptchaV2InvisibleWidget === null) {",
				'            return;',
				'        }',
				'',
				'        e.preventDefault();',
				'        window.securwpRecaptchaV2InvisiblePendingForm = this;',
				'        grecaptcha.execute(window.securwpRecaptchaV2InvisibleWidget);',
				'    });',
				'});',
			]
		);

		wp_add_inline_script( 'securwp-recaptcha-v2-invisible', $inline_js, 'before' );
	}

    /**
     * Enqueue Cloudflare Turnstile scripts.
     *
     * @param string $site_key The Turnstile site key.
     * @return void
     */
	private function enqueue_turnstile_scripts( string $site_key ): void {
		// phpcs:ignore PluginCheck.CodeAnalysis.EnqueuedResourceOffloading.OffloadedContent -- Optional Cloudflare Turnstile service integration requires Cloudflare's hosted client script.
		wp_enqueue_script(
			'securwp-turnstile',
			'https://challenges.cloudflare.com/turnstile/v0/api.js',
			[],
			SECURWP_VERSION,
			true
		);

        $inline_js = <<<'JS'
function securwpTurnstileCallback(token) {
    var field = document.getElementById('securwp_captcha_token');
    if (field) {
        field.value = token;
    }
}
JS;

        wp_add_inline_script( 'securwp-turnstile', $inline_js );
    }

    /**
     * Enqueue shared login-page styles for visible CAPTCHA providers.
     */
    private function enqueue_login_styles(): void {
        if ( wp_style_is( 'securwp-login-captcha', 'enqueued' ) ) {
            return;
        }

		wp_register_style( 'securwp-login-captcha', false, [], SECURWP_VERSION );
		wp_enqueue_style( 'securwp-login-captcha' );

        $inline_css = <<<'CSS'
#loginform .securwp-login-captcha {
    clear: both;
    display: flex;
    justify-content: center;
    width: 100%;
    max-width: 100%;
    margin: 12px 0 16px;
    overflow: hidden;
}

#loginform .securwp-login-captcha .cf-turnstile,
#loginform .securwp-login-captcha .g-recaptcha {
    max-width: 100%;
}

#loginform .securwp-login-captcha .g-recaptcha > div,
#loginform .securwp-login-captcha .g-recaptcha iframe,
#loginform .securwp-login-captcha .cf-turnstile iframe {
    max-width: 100%;
}
CSS;

        wp_add_inline_style( 'securwp-login-captcha', $inline_css );
    }
}
