<?php
/**
 * Concrete PayPal webhook signature verifier.
 *
 * @since   1.0.0
 * @package DonationRouter
 */

declare( strict_types=1 );
namespace DonationRouter;

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

/**
 * Verifies PayPal webhook signatures via the PayPal REST API.
 *
 * Caches each verification result for 5 minutes keyed on the PayPal
 * Transmission-ID header so repeated delivery attempts (PayPal retries)
 * do not trigger redundant API calls.
 *
 * Sensitive fields (tokens, full payloads) are never written to the error log.
 *
 * @since 1.0.0
 */
class PayPal_Webhook_Signature_Verifier implements Webhook_Signature_Verifier_Interface {

	/**
	 * PayPal sandbox base URL.
	 *
	 * @since 1.0.0
	 *
	 * @var string
	 */
	private const SANDBOX_BASE = 'https://api.sandbox.paypal.com';

	/**
	 * PayPal live base URL.
	 *
	 * @since 1.0.0
	 *
	 * @var string
	 */
	private const LIVE_BASE = 'https://api.paypal.com';

	/**
	 * Verify that a webhook event was genuinely sent by PayPal.
	 *
	 * The transmission ID uniquely identifies a single delivery attempt.
	 * Results are cached under `ador_whs_{transmission_id}` for 5 minutes
	 * so concurrent retries from PayPal do not multiply API calls.
	 *
	 * @since 1.0.0
	 *
	 * @param object              $event        Decoded JSON event object.
	 * @param array<string,mixed> $headers      HTTP headers (lowercase, underscored keys as WP delivers; hyphenated also accepted).
	 * @param string              $webhook_id   PayPal webhook ID registered for this account.
	 * @param string              $access_token Bearer token for the account.
	 * @param string              $mode         Account environment ('live' or 'sandbox').
	 *
	 * @return bool
	 */
	public function verify( object $event, array $headers, string $webhook_id, string $access_token, string $mode = 'live' ): bool {
		if ( '' === $access_token || '' === $webhook_id ) {
			return false;
		}

		// WP_REST_Request::get_headers() delivers lowercase, underscored keys
		// (e.g. `paypal_transmission_id`); normalise so hyphenated inputs work too.
		$headers         = $this->normalize_headers( $headers );
		$transmission_id = (string) ( $headers['paypal_transmission_id'] ?? '' );
		$cache_key       = 'ador_whs_' . sanitize_key( $transmission_id );

		if ( '' !== $transmission_id ) {
			$cached = get_transient( $cache_key );
			if ( false !== $cached ) {
				return '1' === $cached;
			}
		}

		$verified = $this->call_paypal_api( $event, $headers, $webhook_id, $access_token, $mode );

		if ( '' !== $transmission_id ) {
			set_transient( $cache_key, $verified ? '1' : '0', 5 * MINUTE_IN_SECONDS );
		}

		return $verified;
	}

	/**
	 * Call PayPal's verify-webhook-signature endpoint.
	 *
	 * @since 1.0.0
	 *
	 * @param object              $event         Decoded event payload.
	 * @param array<string,mixed> $headers       Header map, already normalised to lowercase, underscored keys.
	 * @param string              $webhook_id    Registered PayPal webhook ID.
	 * @param string              $access_token  Bearer token for the account.
	 * @param string              $mode          Account environment ('live' or 'sandbox').
	 *
	 * @return bool
	 */
	private function call_paypal_api( object $event, array $headers, string $webhook_id, string $access_token, string $mode ): bool {
		$base = 'sandbox' === $mode ? self::SANDBOX_BASE : self::LIVE_BASE;

		$body = array(
			'transmission_id'   => $headers['paypal_transmission_id'] ?? '',
			'transmission_time' => $headers['paypal_transmission_time'] ?? '',
			'transmission_sig'  => $headers['paypal_transmission_sig'] ?? '',
			'cert_url'          => $headers['paypal_cert_url'] ?? '',
			'auth_algo'         => $headers['paypal_auth_algo'] ?? '',
			'webhook_id'        => $webhook_id,
			'webhook_event'     => $event,
		);

		$encoded = wp_json_encode( $body );
		if ( false === $encoded ) {
			return false;
		}

		$response = wp_remote_post(
			$base . '/v1/notifications/verify-webhook-signature',
			array(
				'headers' => array(
					'Authorization' => 'Bearer ' . $access_token,
					'Content-Type'  => 'application/json',
				),
				'body'    => $encoded,
				'timeout' => 15,
			)
		);

		if ( is_wp_error( $response ) ) {
			// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
			error_log( '[ADOR] Webhook signature API call failed: ' . $response->get_error_message() );
			return false;
		}

		if ( 200 !== (int) wp_remote_retrieve_response_code( $response ) ) {
			return false;
		}

		$result = json_decode( wp_remote_retrieve_body( $response ) );
		return is_object( $result ) && ( $result->verification_status ?? '' ) === 'SUCCESS';
	}

	/**
	 * Normalise HTTP header keys to WordPress's canonical form: lowercase with
	 * hyphens converted to underscores — the shape `WP_REST_Request::get_headers()`
	 * always produces (e.g. `Paypal-Transmission-Id` → `paypal_transmission_id`).
	 * Hyphenated input is accepted too, so the verifier is agnostic to how its
	 * caller obtained the headers.
	 *
	 * @since 1.0.0
	 *
	 * @param array<string,mixed> $headers Raw header map.
	 *
	 * @return array<string,mixed> Header map keyed by lowercase, underscored names.
	 */
	private function normalize_headers( array $headers ): array {
		$normalized = array();
		foreach ( $headers as $key => $value ) {
			$normalized[ str_replace( '-', '_', strtolower( (string) $key ) ) ] = $value;
		}
		return $normalized;
	}
}
