<?php
/**
 * File Path: includes/payment/class-mbs-paypal-adapter.php
 *
 * PayPal Payment Adapter
 *
 * Implements MBS_Payment_Processor for PayPal IPN (Instant Payment
 * Notification) events.
 *
 * Webhook setup (production):
 *   1. In the PayPal account under Account Settings → Notifications → IPN,
 *      enable IPN and set the notification URL to:
 *      https://yoursite.com/wp-json/mbs/v1/webhook/paypal
 *   2. In each PayPal button/product, set the custom field to encode the tier:
 *      custom = mbs_tier:pro   (for the PRO product)
 *      custom = mbs_tier:pub   (for the Publisher product)
 *   3. Toggle sandbox mode in Settings → License → Payment → PayPal Sandbox.
 *      E-002: Defaults to false (live). Enable sandbox mode explicitly in
 *      settings for local/staging testing.
 *
 * Signature verification (IPN protocol):
 *   PayPal IPN does not use a shared secret or HMAC. Instead, verification
 *   requires POSTing the raw IPN body back to PayPal's IPN verification URL
 *   with "cmd=_notify-validate" prepended. PayPal responds with either
 *   "VERIFIED" or "INVALID". Only VERIFIED events are processed.
 */

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

class MBS_PayPal_Adapter implements MBS_Payment_Processor {

	/** PayPal live IPN verification endpoint. */
	const VERIFY_URL_LIVE    = 'https://ipnpb.paypal.com/cgi-bin/webscr';

	/** PayPal sandbox IPN verification endpoint. */
	const VERIFY_URL_SANDBOX = 'https://ipnpb.sandbox.paypal.com/cgi-bin/webscr';

	/** wp_options key for sandbox toggle. */
	const SANDBOX_OPTION = 'mbs_setting_payment_paypal_sandbox';

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

	/**
	 * Register the PayPal IPN REST endpoint.
	 * Hook: rest_api_init
	 */
	/**
	 * Register the rest_api_init hook for the PayPal 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/paypal', 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 an IPN event by posting back to PayPal's verification endpoint.
	 *
	 * Returns true only when PayPal responds exactly "VERIFIED".
	 * Any network failure, non-200 response, or "INVALID" response returns false.
	 *
	 */
	public static function verify_signature( WP_REST_Request $request ): bool {
		$raw_body = $request->get_body();

		if ( empty( $raw_body ) ) {
			return false;
		}

		// E-002: Default is false (live). Previously defaulted to true (sandbox),
		// which caused all real payments on a fresh deployment to be verified
		// against the sandbox endpoint, returning INVALID and issuing no license.
		$use_sandbox = (bool) get_option( self::SANDBOX_OPTION, false );
		$verify_url  = $use_sandbox ? self::VERIFY_URL_SANDBOX : self::VERIFY_URL_LIVE;

		$response = wp_remote_post( $verify_url, array(
			'body'    => 'cmd=_notify-validate&' . $raw_body,
			'timeout' => 20,
			'headers' => array(
				'Content-Type' => 'application/x-www-form-urlencoded',
				'User-Agent'   => 'MyBookShowroom-IPN/' . MBS_VERSION,
			),
		) );

		if ( is_wp_error( $response ) ) {
			// Network failure — return false. handle() will still return 200 per
			// IPN protocol; the event will be retried by PayPal automatically.
			return false;
		}

		return 'VERIFIED' === trim( wp_remote_retrieve_body( $response ) );
	}

	/**
	 * Extract normalized payment data from a PayPal IPN POST body.
	 * Returns null when payment_status is not "Completed" or required fields
	 * are absent — these events are acknowledged but not processed.
	 *
	 * Tier is read from the IPN `custom` field using the convention:
	 *   custom = mbs_tier:pro   or   custom = mbs_tier:pub
	 *
	 * @return array|null
	 */
	public static function extract_payment_data( WP_REST_Request $request ): ?array {
		$ipn = array();
		parse_str( $request->get_body(), $ipn );

		$payment_status = sanitize_text_field( $ipn['payment_status'] ?? '' );

		if ( 'Completed' !== $payment_status ) {
			return null;
		}

		$email    = sanitize_email( $ipn['payer_email'] ?? '' );
		$tx_id    = sanitize_text_field( $ipn['txn_id'] ?? '' );
		$amount   = (float) ( $ipn['mc_gross'] ?? 0 );
		$currency = strtoupper( sanitize_text_field( $ipn['mc_currency'] ?? 'USD' ) );
		$custom   = sanitize_text_field( $ipn['custom'] ?? '' );

		// Parse tier from custom field: "mbs_tier:pro", "mbs_tier:pub", or "mbs_tier:dev".
		// dev is a sandbox/testing alias that maps to pub tier.
		$tier = '';
		if ( preg_match( '/mbs_tier:(pro|pub|dev)/i', $custom, $m ) ) {
			$tier = strtolower( $m[1] );
			if ( 'dev' === $tier ) {
				$tier = 'pub'; // dev tier maps to pub for provisioning purposes
			}
		}

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

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

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

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

	/**
	 * Handle an incoming PayPal IPN POST.
	 * Called by WordPress REST API on POST /wp-json/mbs/v1/webhook/paypal.
	 *
	 * Per PayPal IPN protocol, always return HTTP 200 — even for invalid or
	 * unrecognized events — otherwise PayPal will retry for up to 4 days.
	 *
	 * @return WP_REST_Response Always 200.
	 */
	public static function handle( WP_REST_Request $request ): WP_REST_Response {
		if ( ! static::verify_signature( $request ) ) {
			// Must still return 200 per IPN protocol, but do not process.
			return new WP_REST_Response( null, 200 );
		}

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

		if ( null === $data ) {
			return new WP_REST_Response( null, 200 );
		}

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

		if ( $ok ) {
			do_action( 'mbs_payment_completed', $data['email'], $data['tier'], 'paypal', $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( null, 200 );
	}

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

	/**
	 * POST payment data to the API server's /license/provision endpoint.
	 *
	 * On failure, fires do_action( 'mbs_provision_failed', 'paypal', $data, $code )
	 * so the failure can be handled (e.g. stored as a transient for admin notice)
	 * 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
				'paypal',
				$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', 'paypal', $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', 'paypal', $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;
	}
}
