<?php
/**
 * File Path: includes/payment/class-mbs-stripe-adapter.php
 *
 * Stripe Payment Adapter
 *
 * Implements MBS_Payment_Processor for Stripe Checkout webhook events.
 *
 * Webhook setup (production):
 *   1. In the Stripe dashboard, create a webhook endpoint pointing to:
 *      https://yoursite.com/wp-json/mbs/v1/webhook/stripe
 *   2. Enable only the event: checkout.session.completed
 *   3. Copy the signing secret (whsec_...) to:
 *      Settings → License → Payment → Stripe Webhook Secret
 *      (stored encrypted via MBS_Encryption, never logged)
 *   4. In each Stripe product's metadata, add:
 *      mbs_tier = pro   (for the PRO product)
 *      mbs_tier = pub   (for the Publisher product)
 *
 * Signature verification:
 *   Stripe signs every event with HMAC-SHA256 over the string
 *   "{timestamp}.{raw_payload}" using the endpoint signing secret.
 *   The Stripe-Signature header format:
 *     t=<unix_timestamp>,v1=<hex_hmac>[,v1=<hex_hmac>...]
 *   Events older than MBS_HMAC_WINDOW (300s) are rejected.
 */

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

class MBS_Stripe_Adapter implements MBS_Payment_Processor {

	/**
	 * wp_options key where the encrypted Stripe webhook secret is stored.
	 * Set via the admin payment settings tab (Sprint 9+).
	 */
	const WEBHOOK_SECRET_OPTION = 'mbs_setting_payment_stripe_webhook_secret_encrypted';

	// =========================================================================
	// Interface implementation
	// =========================================================================

	/**
	 * Register the Stripe webhook REST endpoint.
	 * Hook: rest_api_init
	 */
	/**
	 * Register the rest_api_init hook for the Stripe webhook 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_webhook' ) );
	}

	public static function register_webhook(): void {
		register_rest_route( 'mbs/v1', '/webhook/stripe', array(
			'methods'             => WP_REST_Server::CREATABLE,
			'callback'            => array( __CLASS__, 'handle' ),
			// Stripe requires an unauthenticated public endpoint; authentication is performed
			// via Stripe-Signature HMAC verification in verify_signature(). See SECURITY.md Rule S14-3.
			'permission_callback' => '__return_true',
		) );
	}

	/** Verify the Stripe-Signature header using HMAC-SHA256. */
	public static function verify_signature( WP_REST_Request $request ): bool {
		$raw_body = $request->get_body();
		$header   = $request->get_header( 'Stripe-Signature' );
		$secret   = mbs_decrypt_option( self::WEBHOOK_SECRET_OPTION );

		if ( empty( $secret ) || empty( $header ) || empty( $raw_body ) ) {
			return false;
		}

		$timestamp  = '';
		$signatures = array();

		foreach ( explode( ',', $header ) as $part ) {
			$pair = explode( '=', $part, 2 );
			if ( count( $pair ) !== 2 ) {
				continue;
			}
			if ( 't' === $pair[0] ) {
				$timestamp = $pair[1];
			} elseif ( 'v1' === $pair[0] ) {
				$signatures[] = $pair[1];
			}
		}

		if ( empty( $timestamp ) || empty( $signatures ) ) {
			return false;
		}

		// Reject events outside the 5-minute replay window (MBS_HMAC_WINDOW).
		if ( abs( time() - (int) $timestamp ) > MBS_HMAC_WINDOW ) {
			return false;
		}

		$expected = hash_hmac( 'sha256', $timestamp . '.' . $raw_body, $secret );

		foreach ( $signatures as $sig ) {
			if ( hash_equals( $expected, $sig ) ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Extract normalized payment data from a Stripe checkout.session.completed event.
	 * Returns null for any other event type.
	 *
	 * @return array|null
	 */
	public static function extract_payment_data( WP_REST_Request $request ): ?array {
		$event = json_decode( $request->get_body(), true );

		if ( ! is_array( $event ) || ( $event['type'] ?? '' ) !== 'checkout.session.completed' ) {
			return null;
		}

		$session  = $event['data']['object'] ?? array();
		$email    = sanitize_email( $session['customer_details']['email'] ?? '' );
		$tier     = sanitize_key( $session['metadata']['mbs_tier'] ?? '' );
		$tx_id    = sanitize_text_field( $session['id'] ?? '' );
		$amount   = absint( $session['amount_total'] ?? 0 ) / 100; // Stripe: pence/cents
		$currency = strtoupper( sanitize_text_field( $session['currency'] ?? 'USD' ) );

		if ( empty( $email ) || ! in_array( $tier, array( 'pro', 'pub' ), true ) || empty( $tx_id ) ) {
			return null;
		}

		return array(
			'email'     => $email,
			'tier'      => $tier,
			'tx_id'     => $tx_id,
			'amount'    => $amount,
			'currency'  => $currency,
			'processor' => 'stripe',
		);
	}

	/** Provision a license on the API server after a verified Stripe payment. */
	public static function provision( array $data ): bool {
		return self::call_provision_endpoint( $data );
	}

	// =========================================================================
	// Webhook handler (REST callback)
	// =========================================================================

	/**
	 * Handle an incoming Stripe webhook POST.
	 * Called by WordPress REST API on POST /wp-json/mbs/v1/webhook/stripe.
	 *
	 * @return WP_REST_Response
	 */
	public static function handle( WP_REST_Request $request ): WP_REST_Response {
		if ( ! static::verify_signature( $request ) ) {
			return new WP_REST_Response( array( 'error' => 'Invalid signature.' ), 401 );
		}

		$data = static::extract_payment_data( $request );

		if ( null === $data ) {
			// Event type we don't act on — acknowledge receipt and move on.
			return new WP_REST_Response( array( 'received' => true ), 200 );
		}

		$ok = static::provision( $data );

		if ( ! $ok ) {
			// Return 200 so Stripe does not retry indefinitely. The failure is
			// surfaced via the mbs_provision_failed action hook for admin handling.
			return new WP_REST_Response( array( 'received' => true, 'provisioned' => false ), 200 );
		}

		do_action( 'mbs_payment_completed', $data['email'], $data['tier'], 'stripe', $data['tx_id'], $data['amount'] ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- mbs_/MBS_ prefix is correct; scanner not configured with plugin prefix

		return new WP_REST_Response( array( 'received' => true, 'provisioned' => true ), 200 );
	}

	// =========================================================================
	// Private helpers
	// =========================================================================

	/**
	 * POST payment data to the API server's /license/provision endpoint.
	 *
	 * The server creates the paid license row, logs the transaction in
	 * mbs_transactions, and emails the license key to the customer.
	 *
	 * On failure, fires do_action( 'mbs_provision_failed', 'stripe', $data,
	 * $code, $detail ) so the failure can be handled without writing customer
	 * PII to server logs.
	 *
	 */
	private static function call_provision_endpoint( array $data ): bool {
		// E-001: Customer PII must not travel over plain HTTP in production.
		if ( ! str_starts_with( MBS_API_BASE, 'https://' ) ) {
			do_action(
				'mbs_provision_failed', // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- mbs_/MBS_ prefix is correct; scanner not configured with plugin prefix
				'stripe',
				$data,
				0,
				'Provision aborted: MBS_API_BASE must use https:// in production.'
			);
			return false;
		}

		$response = wp_remote_post( MBS_API_BASE . '/license/provision', array(
			'headers' => array( 'Content-Type' => 'application/json' ),
			'body'    => wp_json_encode( $data ),
			'timeout' => 20,
		) );

		if ( is_wp_error( $response ) ) {
			do_action( 'mbs_provision_failed', 'stripe', $data, 0, $response->get_error_message() ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- mbs_/MBS_ prefix is correct; scanner not configured with plugin prefix
			return false;
		}

		$code = wp_remote_retrieve_response_code( $response );
		$body = json_decode( wp_remote_retrieve_body( $response ), true );

		if ( 201 !== $code || empty( $body['success'] ) ) {
			do_action( 'mbs_provision_failed', 'stripe', $data, $code, wp_remote_retrieve_body( $response ) ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- mbs_/MBS_ prefix is correct; scanner not configured with plugin prefix
			return false;
		}

		return true;
	}
}
