<?php

declare(strict_types=1);

namespace BotGuard\Services\Captcha;

/**
 * Adapter for the token-based remote providers that share the "siteverify"
 * protocol: Google reCAPTCHA, hCaptcha and Cloudflare Turnstile.
 *
 * They differ only in endpoint and site/secret keys, so a single class
 * parameterised by the provider config covers all three. The client renders the
 * widget with the site key and submits a response token, which we POST to the
 * verify endpoint with the secret.
 */
final class RemoteCaptchaProvider implements CaptchaProviderInterface
{
    /**
     * @param array{site_key:string, secret:string, verify_url:string} $config
     */
    public function __construct(private string $provider, private array $config)
    {
    }

    public function name(): string
    {
        return $this->provider;
    }

    public function issue(): array
    {
        // Remote widgets need only the public site key; there is no server answer.
        return [
            'client' => [
                'type'     => $this->provider,
                'site_key' => $this->config['site_key'] ?? '',
            ],
            'answer' => null,
        ];
    }

    public function verify(array $input, ?string $serverAnswer, string $clientIp = ''): CaptchaResult
    {
        $token = (string) ($input['token'] ?? '');
        $secret = (string) ($this->config['secret'] ?? '');

        if ($token === '' || $secret === '') {
            return CaptchaResult::fail($this->name(), ['reason' => 'missing_token_or_secret']);
        }

        $response = $this->post($this->config['verify_url'], [
            'secret'   => $secret,
            'response' => $token,
            'remoteip' => $clientIp,
        ]);

        $success = (bool) ($response['success'] ?? false);
        // reCAPTCHA v3 returns a 0..1 score; require a modest threshold if present.
        if ($success && isset($response['score'])) {
            $success = (float) $response['score'] >= 0.5;
        }

        return new CaptchaResult(
            verified: $success,
            provider: $this->name(),
            solveTimeMs: (int) ($input['solve_time_ms'] ?? 0),
            retries: (int) ($input['retries'] ?? 0),
            detail: $response,
        );
    }

    /**
     * POST form-encoded data and decode the JSON response.
     *
     * @param array<string, string> $data
     * @return array<string, mixed>
     */
    private function post(string $url, array $data): array
    {
        $body = http_build_query($data);

        if (function_exists('curl_init')) {
            $ch = curl_init($url);
            curl_setopt_array($ch, [
                CURLOPT_RETURNTRANSFER => true,
                CURLOPT_POST           => true,
                CURLOPT_POSTFIELDS     => $body,
                CURLOPT_TIMEOUT        => 5,
                CURLOPT_CONNECTTIMEOUT => 3,
            ]);
            $raw = curl_exec($ch);
            curl_close($ch);
        } else {
            $context = stream_context_create([
                'http' => [
                    'method'  => 'POST',
                    'header'  => 'Content-Type: application/x-www-form-urlencoded',
                    'content' => $body,
                    'timeout' => 5,
                ],
            ]);
            $raw = @file_get_contents($url, false, $context);
        }

        $decoded = is_string($raw) ? json_decode($raw, true) : null;

        return is_array($decoded) ? $decoded : ['success' => false, 'error' => 'invalid_response'];
    }
}
