<?php
declare(strict_types=1);

namespace Help4BuilderSuite\Modules\Forms;

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

final class Captcha
{
    public const OPTION_NAME = 'h4bs_forms_captcha_settings';

    private const VERIFY_ENDPOINT = 'https://challenges.cloudflare.com/turnstile/v0/siteverify';

    private const ALWAYS_PASS_TEST_SITE_KEY = '1x00000000000000000000AA';

    private const ALWAYS_PASS_TEST_SECRET_KEY = '1x0000000000000000000000000000000AA';

    /**
     * Return the current site-local CAPTCHA settings. Secrets are opt-in so
     * diagnostics and render paths cannot expose them accidentally.
     *
     * @return array<string, mixed>
     */
    public function settings(bool $includeSecret = false): array
    {
        $stored = get_option(self::OPTION_NAME, []);
        if (! is_array($stored)) {
            $stored = [];
        }

        $provider = sanitize_key((string) ($stored['provider'] ?? 'disabled'));
        if (! in_array($provider, ['disabled', 'turnstile'], true)) {
            $provider = 'disabled';
        }

        $theme = sanitize_key((string) ($stored['theme'] ?? 'auto'));
        if (! in_array($theme, ['auto', 'light', 'dark'], true)) {
            $theme = 'auto';
        }

        $size = sanitize_key((string) ($stored['size'] ?? 'flexible'));
        if (! in_array($size, ['flexible', 'normal', 'compact'], true)) {
            $size = 'flexible';
        }

        $siteKey = $this->sanitizeCredential($stored['site_key'] ?? '');
        $secretKey = $this->sanitizeCredential($stored['secret_key'] ?? '');
        $settings = [
            'provider' => $provider,
            'site_key' => $siteKey,
            'theme' => $theme,
            'size' => $size,
            'configured' => $provider === 'turnstile' && $siteKey !== '' && $secretKey !== '',
            'secret_configured' => $secretKey !== '',
        ];
        if ($includeSecret) {
            $settings['secret_key'] = $secretKey;
        }
        return $settings;
    }

    /**
     * Store one site's provider settings. A blank secret preserves the
     * existing value; clearing it requires an explicit checkbox.
     *
     * @param array<string, mixed> $input
     * @return array<string, mixed>
     */
    public function save(array $input): array
    {
        $existing = $this->settings(true);
        $provider = sanitize_key((string) wp_unslash($input['captcha_provider'] ?? 'disabled'));
        if (! in_array($provider, ['disabled', 'turnstile'], true)) {
            $provider = 'disabled';
        }

        $theme = sanitize_key((string) wp_unslash($input['captcha_theme'] ?? 'auto'));
        if (! in_array($theme, ['auto', 'light', 'dark'], true)) {
            $theme = 'auto';
        }

        $size = sanitize_key((string) wp_unslash($input['captcha_size'] ?? 'flexible'));
        if (! in_array($size, ['flexible', 'normal', 'compact'], true)) {
            $size = 'flexible';
        }

        $siteKey = $this->sanitizeCredential(wp_unslash($input['captcha_site_key'] ?? ''));
        $submittedSecret = $this->sanitizeCredential(wp_unslash($input['captcha_secret_key'] ?? ''));
        $secretKey = ! empty($input['captcha_clear_secret'])
            ? ''
            : ($submittedSecret !== '' ? $submittedSecret : (string) ($existing['secret_key'] ?? ''));

        update_option(
            self::OPTION_NAME,
            [
                'provider' => $provider,
                'site_key' => $siteKey,
                'secret_key' => $secretKey,
                'theme' => $theme,
                'size' => $size,
            ],
            false
        );

        return $this->settings(false);
    }

    /**
     * @param array<string, mixed> $formSettings
     * @return array<string, mixed>
     */
    public function runtime(array $formSettings): array
    {
        $mode = sanitize_key((string) ($formSettings['captcha_mode'] ?? 'inherit'));
        if (! in_array($mode, ['inherit', 'disabled', 'turnstile'], true)) {
            $mode = 'inherit';
        }

        $global = $this->settings(false);
        $requested = $mode !== 'disabled' && (string) ($global['provider'] ?? 'disabled') === 'turnstile';
        return [
            'mode' => $mode,
            'provider' => (string) ($global['provider'] ?? 'disabled'),
            'enabled' => $requested && ! empty($global['configured']),
            'fallback' => $requested && empty($global['configured']),
            'site_key' => (string) ($global['site_key'] ?? ''),
            'theme' => (string) ($global['theme'] ?? 'auto'),
            'size' => (string) ($global['size'] ?? 'flexible'),
        ];
    }

    /**
     * @param array<string, mixed> $payload
     * @param array<string, mixed> $formSettings
     * @return array<string, mixed>
     */
    public function verify(array $payload, int $formId, array $formSettings, string $remoteIp = ''): array
    {
        $runtime = $this->runtime($formSettings);
        if (empty($runtime['enabled'])) {
            return ['ok' => true, 'skipped' => true, 'fallback' => ! empty($runtime['fallback'])];
        }

        $tokenValue = $payload['cf-turnstile-response'] ?? $payload['h4bs_turnstile_response'] ?? '';
        $token = is_scalar($tokenValue) ? trim((string) $tokenValue) : '';
        if ($token === '') {
            return $this->failure('captcha_required', 422, __('Please complete the security verification and try again.', 'help4-builder-suite'));
        }
        if (strlen($token) > 2048 || preg_match('/[\x00-\x1F\x7F]/', $token) === 1) {
            return $this->failure('captcha_invalid', 422, __('Security verification was not accepted. Please try again.', 'help4-builder-suite'));
        }

        $settings = $this->settings(true);
        $requestBody = [
            'secret' => (string) ($settings['secret_key'] ?? ''),
            'response' => $token,
            'idempotency_key' => wp_generate_uuid4(),
        ];
        if ($remoteIp !== '') {
            $requestBody['remoteip'] = sanitize_text_field($remoteIp);
        }

        $response = wp_remote_post(
            self::VERIFY_ENDPOINT,
            [
                'timeout' => 8,
                'redirection' => 0,
                'body' => $requestBody,
                'user-agent' => 'Help4-Builder-Suite/' . H4BS_VERSION . '; ' . home_url('/'),
            ]
        );
        if (is_wp_error($response)) {
            return $this->failure('captcha_unavailable', 503, __('Security verification is temporarily unavailable. Please try again later.', 'help4-builder-suite'));
        }

        $status = (int) wp_remote_retrieve_response_code($response);
        $decoded = json_decode((string) wp_remote_retrieve_body($response), true);
        if ($status < 200 || $status >= 300 || ! is_array($decoded)) {
            return $this->failure('captcha_unavailable', 503, __('Security verification is temporarily unavailable. Please try again later.', 'help4-builder-suite'));
        }

        $expectedAction = 'h4bs_form_' . $formId;
        $returnedAction = sanitize_key((string) ($decoded['action'] ?? ''));
        $expectedHostname = strtolower((string) wp_parse_url(home_url('/'), PHP_URL_HOST));
        $returnedHostname = strtolower(sanitize_text_field((string) ($decoded['hostname'] ?? '')));
        $actionMatches = $returnedAction === '' || hash_equals($expectedAction, $returnedAction);
        $hostnameMatches = $returnedHostname === '' || $expectedHostname === '' || hash_equals($expectedHostname, $returnedHostname);
        $officialAlwaysPassPair = $this->usesOfficialAlwaysPassTestPair($settings);
        if (! empty($decoded['success']) && ($officialAlwaysPassPair || ($actionMatches && $hostnameMatches))) {
            return ['ok' => true, 'provider' => 'turnstile'];
        }

        $providerCodes = is_array($decoded['error-codes'] ?? null) ? $decoded['error-codes'] : [];
        $unavailableCodes = ['missing-input-secret', 'invalid-input-secret', 'internal-error'];
        foreach ($providerCodes as $providerCode) {
            if (in_array(sanitize_key((string) $providerCode), $unavailableCodes, true)) {
                return $this->failure('captcha_unavailable', 503, __('Security verification is temporarily unavailable. Please try again later.', 'help4-builder-suite'));
            }
        }

        return $this->failure('captcha_invalid', 422, __('Security verification was not accepted. Please try again.', 'help4-builder-suite'));
    }

    /**
     * @return array<string, mixed>
     */
    private function failure(string $code, int $status, string $message): array
    {
        return [
            'ok' => false,
            'code' => $code,
            'status' => $status,
            'message' => $message,
        ];
    }

    /**
     * @param mixed $value
     */
    private function sanitizeCredential($value): string
    {
        return substr(trim(sanitize_text_field((string) $value)), 0, 255);
    }

    /**
     * Cloudflare's official always-pass pair may return test placeholders
     * instead of the widget's configured action and hostname. Both exact
     * credentials are required so production and mixed keys remain strict.
     *
     * @param array<string, mixed> $settings
     */
    private function usesOfficialAlwaysPassTestPair(array $settings): bool
    {
        $siteKey = (string) ($settings['site_key'] ?? '');
        $secretKey = (string) ($settings['secret_key'] ?? '');
        return hash_equals(self::ALWAYS_PASS_TEST_SITE_KEY, $siteKey)
            && hash_equals(self::ALWAYS_PASS_TEST_SECRET_KEY, $secretKey);
    }
}
