<?php
/**
 * Registers and removes per-account PayPal webhook subscriptions.
 *
 * @since   1.1.0
 * @package DonationRouter
 */

declare( strict_types=1 );
namespace DonationRouter;

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

/**
 * Creates a PayPal webhook subscription for a managed account, pointing PayPal
 * at this plugin's per-account REST endpoint ({@see Webhook_Router::get_webhook_url()}),
 * and stores the returned webhook ID on the account.
 *
 * Registration uses the account's OWN credentials in its OWN environment, so
 * live and sandbox accounts each register against the correct PayPal API — GiveWP's
 * native onboarding registers only a single global webhook per mode, which cannot
 * disambiguate multiple Business accounts.
 *
 * @since 1.1.0
 */
class Webhook_Registrar {

	/**
	 * PayPal event types to subscribe to — the exact set GiveWP's WebhookRegister
	 * dispatches. Subscribing to events outside this set would receive deliveries
	 * that are then dropped at dispatch, so this list is kept in step with GiveWP.
	 *
	 * @since 1.1.0
	 *
	 * @var string[]
	 */
	public const EVENT_TYPES = array(
		'PAYMENT.CAPTURE.COMPLETED',
		'PAYMENT.CAPTURE.DENIED',
		'PAYMENT.CAPTURE.REFUNDED',
		'CUSTOMER.MERCHANT-INTEGRATION.PRODUCT-SUBSCRIPTION-UPDATED',
	);

	/**
	 * Account data source.
	 *
	 * @since 1.1.0
	 *
	 * @var Account_Repository_Interface
	 */
	private Account_Repository_Interface $account_repo;

	/**
	 * Access-token source.
	 *
	 * @since 1.1.0
	 *
	 * @var PayPal_Access_Token_Client
	 */
	private PayPal_Access_Token_Client $token_client;

	/**
	 * Initialise dependencies.
	 *
	 * @since 1.1.0
	 *
	 * @param Account_Repository_Interface    $account_repo  Account data source.
	 * @param PayPal_Access_Token_Client|null $token_client  Token source; defaults to a real client.
	 */
	public function __construct(
		Account_Repository_Interface $account_repo,
		?PayPal_Access_Token_Client $token_client = null
	) {
		$this->account_repo = $account_repo;
		$this->token_client = $token_client ?? new PayPal_Access_Token_Client();
	}

	/**
	 * Register a PayPal webhook for the account and persist its ID.
	 *
	 * @since 1.1.0
	 *
	 * @param  PayPal_Account $account  The account to register a webhook for.
	 *
	 * @return string  The PayPal webhook ID.
	 *
	 * @throws \RuntimeException  On credential, token, or PayPal API failure.
	 */
	public function register( PayPal_Account $account ): string {
		$token = $this->token_client->get_token( $account );

		$event_types = array_map(
			static fn( string $name ): array => array( 'name' => $name ),
			self::EVENT_TYPES
		);

		$response = wp_remote_post(
			PayPal_Access_Token_Client::base_url_for( $account ) . '/v1/notifications/webhooks',
			array(
				'headers' => array(
					'Authorization' => 'Bearer ' . $token,
					'Content-Type'  => 'application/json',
				),
				'body'    => (string) wp_json_encode(
					array(
						'url'         => Webhook_Router::get_webhook_url( (int) $account->id ),
						'event_types' => $event_types,
					)
				),
				'timeout' => 15,
			)
		);

		if ( is_wp_error( $response ) ) {
			throw new \RuntimeException( esc_html( 'PayPal webhook registration failed: ' . $response->get_error_message() ) );
		}

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

		if ( 201 !== $code ) {
			$name = is_array( $body ) && isset( $body['name'] ) ? (string) $body['name'] : (string) $code;
			throw new \RuntimeException( esc_html( 'PayPal rejected the webhook registration (' . $name . ').' ) );
		}

		$webhook_id = is_array( $body ) && isset( $body['id'] ) ? (string) $body['id'] : '';
		if ( '' === $webhook_id ) {
			throw new \RuntimeException( esc_html__( 'PayPal did not return a webhook ID.', 'alamin-donation-router' ) );
		}

		$this->account_repo->update_webhook_id( (int) $account->id, $webhook_id );

		return $webhook_id;
	}

	/**
	 * Delete the account's PayPal webhook and clear the stored ID.
	 *
	 * A webhook that is already gone at PayPal (404) is treated as success so the
	 * stored ID is cleared either way.
	 *
	 * @since 1.1.0
	 *
	 * @param  PayPal_Account $account  The account whose webhook to remove.
	 *
	 * @return bool  True when the webhook was removed (or was already absent).
	 *
	 * @throws \RuntimeException  On token or transport failure.
	 */
	public function deregister( PayPal_Account $account ): bool {
		if ( null === $account->webhook_id || '' === $account->webhook_id ) {
			return false;
		}

		$token = $this->token_client->get_token( $account );

		$response = wp_remote_request(
			PayPal_Access_Token_Client::base_url_for( $account ) . '/v1/notifications/webhooks/' . rawurlencode( $account->webhook_id ),
			array(
				'method'  => 'DELETE',
				'headers' => array( 'Authorization' => 'Bearer ' . $token ),
				'timeout' => 15,
			)
		);

		if ( is_wp_error( $response ) ) {
			throw new \RuntimeException( esc_html( 'PayPal webhook deletion failed: ' . $response->get_error_message() ) );
		}

		$code = (int) wp_remote_retrieve_response_code( $response );
		if ( 204 === $code || 404 === $code ) {
			$this->account_repo->update_webhook_id( (int) $account->id, null );
			return true;
		}

		// Any other status (401/403/422/5xx) is a real failure: the webhook still
		// exists at PayPal, so the stored ID is deliberately kept. Log it — a bare
		// false is otherwise indistinguishable from the "nothing to delete" return
		// above, hiding a webhook that will keep delivering.
		$body = json_decode( (string) wp_remote_retrieve_body( $response ), true );
		$name = is_array( $body ) && isset( $body['name'] ) ? (string) $body['name'] : (string) $code;
		// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
		error_log(
			sprintf(
				'[ADOR] Webhook deregistration failed. account_id=%d status=%d reason=%s',
				(int) $account->id,
				$code,
				$name
			)
		);

		return false;
	}
}
