<?php
/**
 * Adapter over GiveWP's PayPal Commerce onboarding services.
 *
 * @since   1.1.0
 * @package DonationRouter
 */

declare( strict_types=1 );
namespace DonationRouter;

use Give\PaymentGateways\PayPalCommerce\PayPalClient;
use Give\PaymentGateways\PayPalCommerce\Repositories\PayPalAuth;

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

/**
 * Reuses GiveWP's own (approved-partner) PayPal onboarding to connect any number
 * of PayPal Business accounts into this plugin's managed accounts — GiveWP core
 * stores only one account per mode, so its onboarding services are reused here
 * but the result is captured into this plugin's own storage instead of GiveWP's
 * single global slot.
 *
 * All access goes through the `give()` container; none of these calls write
 * GiveWP's `give_paypal_commerce_*` options — they only fetch a referral link
 * and exchange the returned authorization code for the seller's REST credentials.
 *
 * Every accessor fails safe: GiveWP being inactive or a service call throwing is
 * surfaced as a WP_Error, never a fatal.
 *
 * @since 1.1.0
 */
class GiveWP_Onboarding_Bridge {

	/**
	 * Whether GiveWP's PayPal Commerce onboarding services are available.
	 *
	 * @since 1.1.0
	 *
	 * @return bool
	 */
	public function is_available(): bool {
		return null !== $this->auth() && null !== $this->client();
	}

	/**
	 * Fetch a PayPal partner referral link for onboarding a new seller in the
	 * given environment, reusing GiveWP's approved partner.
	 *
	 * @since 1.1.0
	 *
	 * @param  string $mode        'live' or 'sandbox'.
	 * @param  string $return_url  URL PayPal returns the merchant to after onboarding.
	 *
	 * @return array{action_url:string,nonce:string}|\WP_Error
	 */
	public function partner_referral( string $mode, string $return_url ) {
		$auth = $this->auth();
		if ( null === $auth ) {
			return new \WP_Error( 'ador_givewp_unavailable', __( 'GiveWP PayPal onboarding is unavailable.', 'alamin-donation-router' ) );
		}

		if ( ! $this->set_mode( $mode ) ) {
			return new \WP_Error( 'ador_givewp_unavailable', __( 'GiveWP PayPal onboarding is unavailable.', 'alamin-donation-router' ) );
		}

		try {
			$link = $auth->getSellerPartnerLink( $return_url, 'PPCP' );
		} catch ( \Throwable $e ) {
			return new \WP_Error( 'ador_partner_link_failed', $e->getMessage() );
		}

		if ( ! is_array( $link ) ) {
			return new \WP_Error( 'ador_partner_link_failed', __( 'PayPal did not return an onboarding link.', 'alamin-donation-router' ) );
		}

		$nonce      = (string) ( $link['nonce'] ?? '' );
		$action_url = (string) ( $link['partnerLink'] ?? '' );

		if ( '' === $action_url || '' === $nonce ) {
			return new \WP_Error( 'ador_partner_link_failed', __( 'PayPal returned an incomplete onboarding link.', 'alamin-donation-router' ) );
		}

		return array(
			'action_url' => $action_url,
			'nonce'      => $nonce,
		);
	}

	/**
	 * Exchange the authorization code returned by PayPal's onboarding callback
	 * for the seller's REST API credentials, in this plugin's credential shape.
	 *
	 * @since 1.1.0
	 *
	 * @param  string $mode       'live' or 'sandbox'.
	 * @param  string $auth_code  Authorization code from PayPal's onboarding callback.
	 * @param  string $shared_id  Shared ID from PayPal's onboarding callback.
	 * @param  string $nonce      The seller nonce from the partner referral.
	 *
	 * @return array{clientId:string,clientSecret:string,token:array<string,mixed>}|\WP_Error
	 */
	public function exchange_credentials( string $mode, string $auth_code, string $shared_id, string $nonce ) {
		$auth = $this->auth();
		if ( null === $auth ) {
			return new \WP_Error( 'ador_givewp_unavailable', __( 'GiveWP PayPal onboarding is unavailable.', 'alamin-donation-router' ) );
		}

		if ( ! $this->set_mode( $mode ) ) {
			return new \WP_Error( 'ador_givewp_unavailable', __( 'GiveWP PayPal onboarding is unavailable.', 'alamin-donation-router' ) );
		}

		try {
			$seller_token = $auth->getTokenFromAuthorizationCode( $auth_code, $shared_id, $nonce );
			$access_token = is_array( $seller_token ) ? (string) ( $seller_token['accessToken'] ?? '' ) : '';

			if ( '' === $access_token ) {
				return new \WP_Error( 'ador_token_exchange_failed', __( 'Could not obtain a seller access token from PayPal.', 'alamin-donation-router' ) );
			}

			$rest = (array) $auth->getSellerRestAPICredentials( $access_token );
		} catch ( \Throwable $e ) {
			return new \WP_Error( 'ador_credential_exchange_failed', $e->getMessage() );
		}

		$client_id     = (string) ( $rest['client_id'] ?? '' );
		$client_secret = (string) ( $rest['client_secret'] ?? '' );

		if ( '' === $client_id || '' === $client_secret ) {
			return new \WP_Error( 'ador_credential_exchange_failed', __( 'PayPal did not return seller REST API credentials.', 'alamin-donation-router' ) );
		}

		try {
			$token = (array) $auth->getTokenFromClientCredentials( $client_id, $client_secret );
		} catch ( \Throwable $e ) {
			// The seller REST credentials were obtained, but a client-credentials
			// token could not be minted. Keep the empty-token fallback (GiveWP
			// re-mints from clientId/secret at checkout) yet log it: this is the
			// earliest signal that the freshly-issued credentials might not actually
			// authenticate, which would otherwise resurface only at a donor's
			// checkout rather than at connect time.
			// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
			error_log( '[ADOR] Seller token mint failed after credential exchange: ' . $e->getMessage() );
			$token = array(
				'accessToken' => '',
				'expiresIn'   => 0,
				'tokenType'   => 'Bearer',
			);
		}

		return array(
			'clientId'     => $client_id,
			'clientSecret' => $client_secret,
			'token'        => $token,
		);
	}

	/**
	 * Pin GiveWP's mode-dependent PayPal services to the requested environment.
	 *
	 * Returns false when GiveWP's PayPalClient cannot be resolved: callers must
	 * treat that as an error rather than proceeding, otherwise the live/sandbox
	 * pin is silently skipped and credentials could be exchanged against whatever
	 * environment the client currently holds.
	 *
	 * @since 1.1.0
	 *
	 * @param  string $mode  'live' or 'sandbox'.
	 *
	 * @return bool  True when the mode was pinned; false when the client is unavailable.
	 */
	private function set_mode( string $mode ): bool {
		$mode   = 'sandbox' === $mode ? 'sandbox' : 'live';
		$client = $this->client();

		if ( null === $client ) {
			return false;
		}

		$client->setMode( $mode );
		return true;
	}

	/**
	 * Resolve GiveWP's PayPalAuth repository from the container.
	 *
	 * @since 1.1.0
	 *
	 * @return PayPalAuth|null
	 */
	private function auth(): ?PayPalAuth {
		try {
			return give( PayPalAuth::class );
		} catch ( \Throwable $e ) {
			return null;
		}
	}

	/**
	 * Resolve GiveWP's PayPalClient from the container.
	 *
	 * @since 1.1.0
	 *
	 * @return PayPalClient|null
	 */
	private function client(): ?PayPalClient {
		try {
			return give( PayPalClient::class );
		} catch ( \Throwable $e ) {
			return null;
		}
	}
}
