<?php
/**
 * PayPal Connector — Actions
 *
 * Implements the PayPal payment-initiation path called directly by
 * PayPalConnector::initiate_payment().
 *
 * PayPal Orders API v2 overview:
 *  Base URL  : https://api-m.paypal.com (live) or https://api-m.sandbox.paypal.com (sandbox)
 *  Auth      : OAuth 2.0 client_credentials — POST /v1/oauth2/token with Basic client_id:secret
 *              Access token is cached in a transient to avoid repeated token requests.
 *  Create    : POST /v2/checkout/orders  — returns order ID + buyer approval link
 *  Capture   : POST /v2/checkout/orders/{id}/capture — called from WebhookHandler on approval
 *
 * @package AmbassadorHub\Connectors\PayPal
 */

declare( strict_types=1 );

namespace AmbassadorHub\Connectors\PayPal;

use AmbassadorHub\Connectors\ConnectorResult;

use function apply_filters;
use function delete_transient;
use function esc_html__;
use function get_option;
use function get_transient;
use function home_url;
use function is_wp_error;
use function number_format;
use function set_transient;
use function strtoupper;
use function wp_json_encode;
use function wp_remote_post;
use function wp_remote_retrieve_body;
use function wp_remote_retrieve_response_code;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Executes PayPal payment-initiation requests.
 */
final class Actions {

	/** Transient key for the cached OAuth access token. */
	private const TOKEN_TRANSIENT = 'ambh_paypal_access_token';

	/**
	 * Create a PayPal order via the Orders API v2.
	 *
	 * Called directly by PayPalConnector::initiate_payment().
	 *
	 * @param array<string, mixed> $donation_data Normalised donation payload.
	 * @return ConnectorResult
	 */
	public static function execute_payment_initiation( array $donation_data ): ConnectorResult {
		$access_token = self::get_access_token();
		if ( empty( $access_token ) ) {
			return ConnectorResult::fail(
				esc_html__( 'Could not obtain a PayPal access token. Check your client ID and secret.', 'ambassadorhub' )
			);
		}

		$amount   = (float) ( $donation_data['amount'] ?? 0 );
		$currency = strtoupper( (string) ( $donation_data['currency'] ?? 'EUR' ) );

		$payload = array(
			'intent'              => 'CAPTURE',
			'purchase_units'      => array(
				array(
					'custom_id'   => (string) ( $donation_data['donation_id'] ?? '' ),
					'description' => $donation_data['description'] ?? '',
					'amount'      => array(
						'currency_code' => $currency,
						'value'         => number_format( $amount, 2, '.', '' ),
					),
				),
			),
			'application_context' => array(
				'return_url'  => $donation_data['redirect_url'] ?? home_url( '/' ),
				'cancel_url'  => $donation_data['cancel_url'] ?? home_url( '/' ),
				'user_action' => 'PAY_NOW',
				'brand_name'  => get_option( 'blogname', '' ),
			),
		);

		/**
		 * Allow other plugins to alter the PayPal order payload before dispatch.
		 *
		 * @param array $payload       Assembled PayPal order data.
		 * @param array $donation_data Original donation data.
		 */
		$payload = apply_filters( 'ambh_paypal_order_payload', $payload, $donation_data );

		// External service call — see readme.txt's "External Services" section
		// (PayPal) for what's sent and when. Order creation: donation amount,
		// currency, and return/cancel URLs, sent when a donor pays via PayPal.
		$response = wp_remote_post(
			trailingslashit( PayPalPspConnector::get_api_base() ) . 'v2/checkout/orders',
			array(
				'headers' => array(
					'Authorization' => 'Bearer ' . $access_token,
					'Content-Type'  => 'application/json',
				),
				'body'    => wp_json_encode( $payload ),
				'timeout' => 15,
			)
		);

		if ( is_wp_error( $response ) ) {
			return ConnectorResult::fail( $response->get_error_message() );
		}

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

		if ( $code < 200 || $code >= 300 ) {
			$error = $body['message'] ?? $body['error_description'] ?? esc_html__( 'Unknown PayPal API error.', 'ambassadorhub' );
			return ConnectorResult::fail( (string) $error );
		}

		$order_id     = $body['id'] ?? null;
		$approve_link = null;

		foreach ( (array) ( $body['links'] ?? array() ) as $link ) {
			if ( ( $link['rel'] ?? '' ) === 'approve' ) {
				$approve_link = $link['href'];
				break;
			}
		}

		if ( ! $order_id || ! $approve_link ) {
			return ConnectorResult::fail(
				esc_html__( 'PayPal did not return an approval URL.', 'ambassadorhub' )
			);
		}

		return ConnectorResult::ok( $order_id, $approve_link );
	}

	// -------------------------------------------------------------------------
	// OAuth helpers
	// -------------------------------------------------------------------------

	/**
	 * Return a valid PayPal access token.
	 *
	 * Tokens are cached in a transient until they expire.  On expiry (or when
	 * no token is stored) a fresh client_credentials grant is requested.
	 *
	 * @return string Access token, or empty string on failure.
	 */
	public static function get_access_token(): string {
		$cached = get_transient( self::TOKEN_TRANSIENT );
		if ( ! empty( $cached ) ) {
			return (string) $cached;
		}

		$client_id     = (string) get_option( PayPalPspConnector::OPTION_CLIENT_ID, '' );
		$client_secret = (string) get_option( PayPalPspConnector::OPTION_CLIENT_SECRET, '' );

		if ( empty( $client_id ) || empty( $client_secret ) ) {
			return '';
		}

		// External service call — see readme.txt's "External Services" section
		// (PayPal). OAuth token exchange: sends the site's own configured
		// client ID/secret, no donor data. Part of every PayPal payment flow.
		$response = wp_remote_post(
			trailingslashit( PayPalPspConnector::get_api_base() ) . 'v1/oauth2/token',
			array(
				'headers' => array(
					'Authorization' => 'Basic ' . base64_encode( $client_id . ':' . $client_secret ), // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
					'Content-Type'  => 'application/x-www-form-urlencoded',
				),
				'body'    => 'grant_type=client_credentials',
				'timeout' => 10,
			)
		);

		if ( is_wp_error( $response ) ) {
			return '';
		}

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

		$data  = json_decode( wp_remote_retrieve_body( $response ), true );
		$token = $data['access_token'] ?? '';
		$ttl   = (int) ( $data['expires_in'] ?? 3600 );

		if ( ! empty( $token ) ) {
			// Store with a 60-second safety margin so it doesn't expire mid-request.
			set_transient( self::TOKEN_TRANSIENT, $token, max( 60, $ttl - 60 ) );
		}

		return $token;
	}

	/**
	 * Flush the cached access token.
	 *
	 * Call this when credentials are updated so the next request fetches a fresh token.
	 *
	 * @return void
	 */
	public static function flush_token(): void {
		delete_transient( self::TOKEN_TRANSIENT );
	}

	// -------------------------------------------------------------------------
	// Connection test
	// -------------------------------------------------------------------------

	/**
	 * Test the PayPal connection by obtaining a fresh access token.
	 *
	 * Flushes the token cache and performs a new client_credentials grant so
	 * we always test the currently-saved credentials.  The name/email shown
	 * is extracted from the access-token JWT payload when present.
	 *
	 * @return array{success: bool, name: string, email: string, message: string}
	 */
	public static function test_connection(): array {
		self::flush_token();

		$client_id     = (string) get_option( PayPalPspConnector::OPTION_CLIENT_ID, '' );
		$client_secret = (string) get_option( PayPalPspConnector::OPTION_CLIENT_SECRET, '' );

		if ( empty( $client_id ) || empty( $client_secret ) ) {
			return array(
				'success' => false,
				'name'    => '',
				'email'   => '',
				'message' => esc_html__( 'Client ID and secret are required.', 'ambassadorhub' ),
			);
		}

		// External service call — see readme.txt's "External Services" section
		// (PayPal). Triggered only when an admin clicks "Test connection" in
		// Settings → Payments; sends the site's own configured credentials,
		// no donor data.
		$response = wp_remote_post(
			trailingslashit( PayPalPspConnector::get_api_base() ) . 'v1/oauth2/token',
			array(
				'headers'   => array(
					'Authorization' => 'Basic ' . base64_encode( $client_id . ':' . $client_secret ), // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
					'Content-Type'  => 'application/x-www-form-urlencoded',
				),
				'body'      => 'grant_type=client_credentials',
				'timeout'   => 10,
				'sslverify' => ! ( defined( 'WP_DEBUG' ) && WP_DEBUG ),
			)
		);

		$invalid = array(
			'success' => false,
			'name'    => '',
			'email'   => '',
			'message' => esc_html__( 'Incorrect credentials. Please check your client ID and secret.', 'ambassadorhub' ),
		);

		if ( is_wp_error( $response ) ) {
			return $invalid;
		}

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

		if ( 200 !== $code ) {
			return $invalid;
		}

		$token  = $data['access_token'] ?? '';
		$app_id = $data['app_id'] ?? '';

		// Cache the fresh token so payment initiation doesn't immediately re-request it.
		if ( ! empty( $token ) ) {
			$ttl = (int) ( $data['expires_in'] ?? 3600 );
			set_transient( self::TOKEN_TRANSIENT, $token, max( 60, $ttl - 60 ) );
		}

		// Try to extract merchant identity from the JWT payload (best-effort).
		$name  = '';
		$email = '';
		if ( ! empty( $token ) ) {
			$parts = explode( '.', $token );
			if ( isset( $parts[1] ) ) {
				// base64url → base64 padding
				$payload_json = base64_decode( str_pad( strtr( $parts[1], '-_', '+/' ), strlen( $parts[1] ) % 4 === 0 ? strlen( $parts[1] ) : strlen( $parts[1] ) + 4 - strlen( $parts[1] ) % 4, '=' ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
				if ( $payload_json ) {
					$payload = json_decode( $payload_json, true );
					$name    = (string) ( $payload['name'] ?? $payload['displayName'] ?? '' );
					$email   = (string) ( $payload['email'] ?? '' );
				}
			}
		}

		// Fall back to the app_id as the identifier when no name is in the token.
		if ( '' === $name && ! empty( $app_id ) ) {
			$name = $app_id;
		}

		return array(
			'success'        => true,
			'name'           => $name,
			'email'          => $email,
			'mode'           => get_option( PayPalPspConnector::OPTION_MODE, 'sandbox' ),
			'client_id_hint' => substr( $client_id, 0, 8 ) . '…',
			'message'        => '',
		);
	}

	// -------------------------------------------------------------------------
	// Helpers
	// -------------------------------------------------------------------------
}
