<?php
/**
 * Google reCAPTCHA v3 verification.
 *
 * @package SubmissionInbox
 */

namespace SUBIN\Forms;

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

/**
 * Verifies built-in form reCAPTCHA v3 tokens server-side.
 */
class RecaptchaVerifier {
	const VERIFY_ENDPOINT = 'https://www.google.com/recaptcha/api/siteverify';

	/**
	 * Settings repository.
	 *
	 * @var \SUBIN_SettingsRepository
	 */
	private $settings;

	/**
	 * Constructor.
	 *
	 * @param \SUBIN_SettingsRepository $settings Settings repository.
	 */
	public function __construct( \SUBIN_SettingsRepository $settings ) {
		$this->settings = $settings;
	}

	/**
	 * Verify a reCAPTCHA token.
	 *
	 * @param string $token Token from frontend.
	 * @param string $expected_action Expected reCAPTCHA action.
	 * @return array{success:bool,score:float|null,reason:string}
	 */
	public function verify( $token, $expected_action ) {
		$settings        = $this->settings->all();
		$token           = sanitize_text_field( (string) $token );
		$expected_action = sanitize_key( $expected_action );
		$secret_key      = trim( (string) ( $settings['recaptcha_secret_key'] ?? '' ) );
		$threshold       = $this->score_threshold( $settings );

		if ( '' === $token ) {
			return $this->result( false, null, 'missing_token' );
		}

		$preflight = apply_filters( 'subin_recaptcha_pre_verification_result', null, $token, $expected_action );
		if ( is_array( $preflight ) ) {
			return $this->normalize_external_result( $preflight );
		}

		if ( '' === $secret_key ) {
			return $this->result( false, null, 'missing_secret_key' );
		}

		$body = array(
			'secret'   => $secret_key,
			'response' => $token,
		);

		if ( ! empty( $settings['store_ip'] ) && ! empty( $_SERVER['REMOTE_ADDR'] ) ) {
			$body['remoteip'] = sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) );
		}

		$response = wp_remote_post(
			self::VERIFY_ENDPOINT,
			array(
				'timeout' => 5,
				'body'    => $body,
			)
		);

		if ( is_wp_error( $response ) ) {
			return $this->filtered_result( $this->result( false, null, 'request_failed' ), $token, $expected_action );
		}

		$data = json_decode( (string) wp_remote_retrieve_body( $response ), true );
		if ( ! is_array( $data ) ) {
			return $this->filtered_result( $this->result( false, null, 'invalid_response' ), $token, $expected_action );
		}

		$score  = isset( $data['score'] ) && is_numeric( $data['score'] ) ? (float) $data['score'] : null;
		$action = sanitize_key( (string) ( $data['action'] ?? '' ) );

		if ( empty( $data['success'] ) ) {
			return $this->filtered_result( $this->result( false, $score, 'verification_failed' ), $token, $expected_action );
		}

		if ( null === $score || $score < $threshold ) {
			return $this->filtered_result( $this->result( false, $score, 'low_score' ), $token, $expected_action );
		}

		if ( '' !== $action && '' !== $expected_action && $action !== $expected_action ) {
			return $this->filtered_result( $this->result( false, $score, 'action_mismatch' ), $token, $expected_action );
		}

		return $this->filtered_result( $this->result( true, $score, '' ), $token, $expected_action );
	}

	/**
	 * Whether reCAPTCHA is enabled and configured.
	 *
	 * @return bool
	 */
	public function is_configured() {
		$settings = $this->settings->all();

		return ! empty( $settings['recaptcha_enabled'] )
			&& '' !== trim( (string) ( $settings['recaptcha_site_key'] ?? '' ) )
			&& '' !== trim( (string) ( $settings['recaptcha_secret_key'] ?? '' ) );
	}

	/**
	 * Normalize the score threshold.
	 *
	 * @param array<string,mixed> $settings Settings.
	 * @return float
	 */
	private function score_threshold( array $settings ) {
		$threshold = is_numeric( $settings['recaptcha_score_threshold'] ?? null ) ? (float) $settings['recaptcha_score_threshold'] : 0.5;

		return max( 0.0, min( 1.0, $threshold ) );
	}

	/**
	 * Build a sanitized verification result.
	 *
	 * @param bool       $success Success.
	 * @param float|null $score Score.
	 * @param string     $reason Reason code.
	 * @return array{success:bool,score:float|null,reason:string}
	 */
	private function result( $success, $score, $reason ) {
		return array(
			'success' => (bool) $success,
			'score'   => null === $score ? null : (float) $score,
			'reason'  => sanitize_key( $reason ),
		);
	}

	/**
	 * Allow tests and local environments to provide a sanitized verification result.
	 *
	 * @param array{success:bool,score:float|null,reason:string} $result Result.
	 * @param string $token Token.
	 * @param string $expected_action Expected action.
	 * @return array{success:bool,score:float|null,reason:string}
	 */
	private function filtered_result( array $result, $token, $expected_action ) {
		$filtered = apply_filters( 'subin_recaptcha_verification_result', $result, $token, $expected_action );
		if ( ! is_array( $filtered ) ) {
			return $result;
		}

		return $this->normalize_external_result( $filtered );
	}

	/**
	 * Normalize externally supplied verification results.
	 *
	 * @param array<string,mixed> $result External result.
	 * @return array{success:bool,score:float|null,reason:string}
	 */
	private function normalize_external_result( array $result ) {
		return $this->result(
			! empty( $result['success'] ),
			isset( $result['score'] ) && is_numeric( $result['score'] ) ? (float) $result['score'] : null,
			(string) ( $result['reason'] ?? '' )
		);
	}
}
