<?php
/**
 * File Path: includes/class-mbs-esp-arc-signup.php
 *
 * ARC Self-Signup REST Handler -- My Book Showroom
 *
 * Registers POST /wp-json/mbs/v1/arc/apply for unauthenticated ARC reviewer
 * self-nomination. Implements a multi-layer spam mitigation pipeline:
 * nonce, honeypot, IP rate limit, CAPTCHA, email dedup. All silent failures
 * return HTTP 200 so bots receive no signal.
 *
 * Sprint F-027: ARC Signup Block.
 *
 * @package    My_Book_Showroom
 * @subpackage Includes
 * @since      1.2.0
 */

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

class MBS_ESP_Arc_Signup {

	/** Default consent text — shared with block attribute default. */
	const DEFAULT_CONSENT_TEXT = 'I agree to receive occasional updates about this book and future releases. You can withdraw consent at any time.';

	// -------------------------------------------------------------------------
	// Route registration
	// -------------------------------------------------------------------------

	/**
	 * Register the REST route. Called on rest_api_init.
	 */
	/**
	 * Register the rest_api_init hook for the ARC self-signup endpoint.
	 * Moved from MBS_Plugin::load_payment_hooks() — REFACTOR-PLAN-ARCH-001 Batch E.
	 */
	public static function register_hooks(): void {
		add_action( 'rest_api_init', array( __CLASS__, 'register_routes' ) );
	}

	public static function register_routes(): void {
		register_rest_route(
			'mbs/v1',
			'/arc/apply',
			array(
				'methods'             => 'POST',
				'callback'            => array( __CLASS__, 'handle_apply' ),
				'permission_callback' => '__return_true',
				// SA-M9: Declare args schema so WP REST API rejects malformed requests
				// before handle_apply() is invoked.
				'args'                => array(
					'post_id' => array(
						'required'          => true,
						'validate_callback' => static function( $value ) {
							return is_numeric( $value ) && (int) $value > 0;
						},
						'sanitize_callback' => 'absint',
					),
					'email'   => array(
						'required'          => true,
						'validate_callback' => 'is_email',
						'sanitize_callback' => 'sanitize_email',
					),
					'name'    => array(
						'required'          => true,
						'validate_callback' => static function( $value ) {
							return is_string( $value ) && '' !== trim( $value );
						},
						'sanitize_callback' => 'sanitize_text_field',
					),
					'nonce'   => array(
						'required'          => true,
						'validate_callback' => static function( $value ) {
							return is_string( $value ) && '' !== $value;
						},
						'sanitize_callback' => 'sanitize_text_field',
					),
				),
			)
		);
	}

	// -------------------------------------------------------------------------
	// Main handler
	// -------------------------------------------------------------------------

	/**
	 * Handle an ARC signup submission.
	 *
	 * All silent-failure paths return 200 OK so bots receive no enumeration signal.
	 *
	 * @param WP_REST_Request $request Incoming REST request.
	 * @return WP_REST_Response|WP_Error
	 */
	public static function handle_apply( WP_REST_Request $request ) {
		$post_id = (int) $request->get_param( 'post_id' );

		// 1. PRO gate -- silent 200.
		if ( ! mbs_is_pro() ) {
			return rest_ensure_response( array( 'status' => 'ok' ) );
		}

		// 2. Nonce verification -- silent 200 on failure.
		$nonce = (string) $request->get_param( 'nonce' );
		if ( ! wp_verify_nonce( $nonce, 'mbs_arc_apply_' . $post_id ) ) {
			return rest_ensure_response( array( 'status' => 'ok' ) );
		}

		// 3. Honeypot -- silent 200 on non-empty value.
		if ( '' !== (string) $request->get_param( 'mbs_hp' ) ) {
			return rest_ensure_response( array( 'status' => 'ok' ) );
		}

		// 4. IP rate limit: max 3 per IP per book per 60 minutes -- silent 200.
		$ip       = self::get_client_ip();
		$rl_key   = 'mbs_arc_rl_' . hash( 'sha256', $ip ) . '_' . $post_id;
		$rl_count = (int) get_transient( $rl_key );
		if ( $rl_count >= 3 ) {
			return rest_ensure_response( array( 'status' => 'ok' ) );
		}

		// 5. Input validation -- 400 on failure (not a bot signal, just bad input).
		$email = sanitize_email( wp_unslash( (string) $request->get_param( 'email' ) ) );
		$name  = sanitize_text_field( wp_unslash( (string) $request->get_param( 'name' ) ) );
		if ( ! is_email( $email ) || '' === $name ) {
			return new WP_Error( 'invalid_input', __( 'Please complete all required fields.', 'my-book-showroom' ), array( 'status' => 400 ) );
		}
		$book = get_post( $post_id );
		if ( ! $book || MBS_POST_TYPE !== $book->post_type || 'publish' !== $book->post_status ) {
			return new WP_Error( 'invalid_book', __( 'Invalid book.', 'my-book-showroom' ), array( 'status' => 400 ) );
		}

		// 6. CAPTCHA verification -- 400 on failure.
		$provider = (string) get_option( 'mbs_setting_email_captcha_provider', '' );
		if ( '' !== $provider ) {
			$token = (string) $request->get_param( 'captcha_token' );
			if ( ! self::verify_captcha( $token, $ip, $provider ) ) {
				return new WP_Error( 'captcha_failed', __( 'Verification failed. Please try again.', 'my-book-showroom' ), array( 'status' => 400 ) );
			}
		}

		// 7. Email dedup -- silent 200 (applicant cannot enumerate existing reviewers).
		if ( MBS_Arc_Applicant_DAO::email_exists_for_book( $email, $post_id ) ) {
			return rest_ensure_response( array( 'status' => 'ok' ) );
		}

		// 8. Insert applicant row.
		$message = sanitize_textarea_field( wp_unslash( (string) $request->get_param( 'message' ) ) );
		$row_id  = MBS_Arc_Applicant_DAO::insert( array(
			'post_id' => $post_id,
			'name'    => $name,
			'email'   => $email,
			'message' => '' !== $message ? $message : null,
			'ip_hash' => hash( 'sha256', $ip ),
		) );

		// Advance rate-limit counter regardless of insert result.
		set_transient( $rl_key, $rl_count + 1, HOUR_IN_SECONDS );

		if ( false === $row_id ) {
			return new WP_Error( 'insert_failed', __( 'Unable to process your application. Please try again.', 'my-book-showroom' ), array( 'status' => 500 ) );
		}

		// 9. Admin notification email.
		self::maybe_send_notification( $name, $email, $message, $book );

		// 10. Success.
		$success_msg = sanitize_text_field( (string) $request->get_param( 'success_message' ) );
		if ( '' === $success_msg ) {
			$success_msg = __( "Thank you! We'll be in touch.", 'my-book-showroom' );
		}
		return rest_ensure_response( array(
			'status'  => 'ok',
			'message' => $success_msg,
		) );
	}

	// -------------------------------------------------------------------------
	// CAPTCHA verification
	// -------------------------------------------------------------------------

	/**
	 * Verify a CAPTCHA token with the selected provider.
	 *
	 * Provider API errors always return false (treat as failed CAPTCHA).
	 * Provider response bodies are never logged or forwarded to the client.
	 *
	 * @param string $token    Token submitted by the browser.
	 * @param string $ip       Client IP address.
	 * @param string $provider Active CAPTCHA provider slug.
	 * @return bool
	 */
	private static function verify_captcha( string $token, string $ip, string $provider ): bool {
		if ( '' === $token ) {
			return false;
		}

		$endpoint   = '';
		$body       = array();
		$pass_score = false;

		switch ( $provider ) {
			case 'recaptcha_v3':
				$secret   = mbs_decrypt_option( 'mbs_setting_email_captcha_recaptcha_v3_secret_key' );
				if ( '' === $secret ) { return false; }
				$endpoint   = 'https://www.google.com/recaptcha/api/siteverify';
				$body       = array( 'secret' => $secret, 'response' => $token, 'remoteip' => $ip );
				$pass_score = true;
				break;

			case 'hcaptcha':
				$secret   = mbs_decrypt_option( 'mbs_setting_email_captcha_hcaptcha_secret_key' );
				if ( '' === $secret ) { return false; }
				$endpoint = 'https://api.hcaptcha.com/siteverify';
				$body     = array( 'secret' => $secret, 'response' => $token, 'remoteip' => $ip );
				break;

			case 'turnstile':
				$secret   = mbs_decrypt_option( 'mbs_setting_email_captcha_turnstile_secret_key' );
				if ( '' === $secret ) { return false; }
				$endpoint = 'https://challenges.cloudflare.com/turnstile/v0/siteverify';
				$body     = array( 'secret' => $secret, 'response' => $token, 'remoteip' => $ip );
				break;

			default:
				return false;
		}

		$response = wp_remote_post( $endpoint, array(
			'body'      => $body,
			'timeout'   => 10,
			'sslverify' => true,
		) );

		if ( is_wp_error( $response ) ) {
			return false;
		}

		$data = json_decode( wp_remote_retrieve_body( $response ), true );
		if ( ! is_array( $data ) || empty( $data['success'] ) ) {
			return false;
		}

		if ( $pass_score ) {
			// reCAPTCHA v3: require score >= 0.5.
			return isset( $data['score'] ) && (float) $data['score'] >= 0.5;
		}

		return true;
	}

	// -------------------------------------------------------------------------
	// Admin notification
	// -------------------------------------------------------------------------

	/**
	 * Send admin notification email on new application, if enabled.
	 *
	 * All user-submitted values pass through sanitize_text_field() before
	 * inclusion in the email body. No HTML in the body -- plain text only.
	 *
	 * @param string  $name    Applicant name (already sanitized).
	 * @param string  $email   Applicant email (already sanitized).
	 * @param string  $message Applicant message (already sanitized).
	 * @param WP_Post $book    Book post object.
	 */
	private static function maybe_send_notification( string $name, string $email, string $message, WP_Post $book ): void {
		if ( '1' !== (string) get_option( 'mbs_setting_arc_signup_notification_enabled', '1' ) ) {
			return;
		}

		$to = (string) get_option( 'mbs_setting_arc_signup_notification_email', '' );
		if ( '' === $to || ! is_email( $to ) ) {
			$to = (string) get_option( 'admin_email' );
		}
		if ( ! is_email( $to ) ) {
			return;
		}

		$book_title  = sanitize_text_field( $book->post_title );
		$edit_url    = admin_url( 'post.php?post=' . $book->ID . '&action=edit' );
		$submitted   = current_time( 'Y-m-d H:i:s' );

		$subject = sprintf(
			/* translators: %s: book title */
			__( 'New ARC Signup Application -- %s', 'my-book-showroom' ),
			$book_title
		);

		$body  = 'Name:      ' . $name . "\n";
		$body .= 'Email:     ' . $email . "\n";
		$body .= 'Book:      ' . $book_title . "\n";
		$body .= 'Message:   ' . ( '' !== $message ? $message : '(none)' ) . "\n";
		$body .= 'Submitted: ' . $submitted . "\n";
		$body .= 'Review:    ' . $edit_url . "\n";

		wp_mail( $to, $subject, $body );
	}

	// -------------------------------------------------------------------------
	// Helper
	// -------------------------------------------------------------------------

	/**
	 * Return the client IP address. Prefers X-Forwarded-For when present
	 * and the server is behind a trusted proxy.
	 *
	 * @return string
	 */
	private static function get_client_ip(): string {
		if ( ! empty( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) {
			$parts = explode( ',', sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) );
			$ip    = trim( $parts[0] );
			if ( filter_var( $ip, FILTER_VALIDATE_IP ) ) {
				return $ip;
			}
		}
		return sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0' ) );
	}
}
