<?php

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

#[\AllowDynamicProperties]

class AffiliateWP_PayPal_API {

	public $credentials;
	private $sandbox = '';

	/**
	 * Get things started
	 *
	 * @access public
	 * @since 2.29.0
	 * @return void
	 */
	public function __construct() {

		if ( affiliate_wp_paypal()->is_test_mode() ) {
			$this->sandbox = 'sandbox.';
		}
	}

	/**
	 * Set sandbox mode for testing specific credentials.
	 *
	 * This allows testing specific credential sets (live or sandbox)
	 * without relying on the global test mode setting.
	 *
	 * @access public
	 * @since 2.33.0
	 * @param bool $enabled Whether to enable sandbox mode.
	 * @return void
	 */
	public function set_sandbox_mode( $enabled ) {
		$this->sandbox = $enabled ? 'sandbox.' : '';
	}

	/**
	 * Set credentials for testing a specific credential set.
	 *
	 * @access public
	 * @since 2.33.0
	 * @param string $client_id The PayPal application Client ID.
	 * @param string $secret    The PayPal application Secret.
	 * @return void
	 */
	public function set_credentials( $client_id, $secret ) {
		$this->credentials = [
			'client_id' => $client_id,
			'secret'    => $secret,
		];
	}

	/**
	 * Process a single referral payment.
	 *
	 * Sends a payout request to PayPal. Returns the decoded PayPal response
	 * on success (201 Created) so the caller can extract the batch ID and
	 * handle payout record creation and status verification.
	 *
	 * @access public
	 * @since 2.29.0
	 * @return array|WP_Error PayPal response array on success, WP_Error on failure.
	 */
	public function send_payment( $args = [] ) {

		$token = $this->get_token();

		if ( is_wp_error( $token ) ) {
			return $token;
		}

		/*
		 * Canonical payout context handed to the customization filters below.
		 * send_bulk_payment() passes the same shape, so a hook reads identical
		 * keys on either path. affiliate_id / referral_id are populated where
		 * the path knows them and 0 otherwise.
		 */
		$context = array(
			'email'        => $args['email'] ?? '',
			'amount'       => $args['amount'] ?? 0,
			'description'  => $args['description'] ?? '',
			'affiliate_id' => (int) ( $args['affiliate_id'] ?? 0 ),
			'referral_id'  => (int) ( $args['referral_id'] ?? 0 ),
		);

		/**
		 * Filters the PayPal payout email subject.
		 *
		 * This is the subject line of the email PayPal sends to the affiliate.
		 *
		 * @since 2.33.0
		 * @param string $subject The email subject.
		 * @param array  $context Payout context: email, amount, description, affiliate_id, referral_id.
		 */
		$email_subject = apply_filters( 'affwp_paypal_payout_email_subject', __( 'Affiliate Earnings Payout', 'affiliate-wp' ), $context );

		/**
		 * Filters the PayPal payout item note.
		 *
		 * This note appears in the affiliate's PayPal transaction details
		 * and notification email. Can be used to customize the payment
		 * description or inject PayPal sandbox test codes.
		 *
		 * @since 2.33.0
		 * @param string $note    The payout note/description.
		 * @param array  $context Payout context: email, amount, description, affiliate_id, referral_id.
		 */
		$note = apply_filters( 'affwp_paypal_payout_note', $context['description'], $context );

		// Build the request payload.
		$payload = [
			'sender_batch_header' => [
				'sender_batch_id' => md5( serialize( $args ) ),
				'email_subject'   => $email_subject,
			],
			'items'               => [
				[
					'recipient_type' => 'EMAIL',
					'amount'         => [
						'value'    => affwp_sanitize_amount( $args['amount'] ),
						'currency' => affwp_get_currency(),
					],
					'receiver'       => $args['email'],
					'note'           => $note,
					'sender_item_id' => (string) $args['referral_id'], // Cast to string for PayPal API.
				],
			],
		];

		$request = wp_remote_post(
			'https://api.' . $this->sandbox . 'paypal.com/v1/payments/payouts?sync_mode=false',
			[
				'headers'     => [
					'Content-Type'                  => 'application/json',
					'Authorization'                 => 'Bearer ' . $token->access_token,
					'PayPal-Partner-Attribution-Id' => 'EasyDigitalDownloads_SP',
				],
				'timeout'     => 45,
				'httpversion' => '1.1',
				'body'        => json_encode( $payload ),
			]
		);

		$body    = wp_remote_retrieve_body( $request );
		$code    = wp_remote_retrieve_response_code( $request );
		$message = wp_remote_retrieve_response_message( $request );

		if ( is_wp_error( $request ) ) {

			affiliate_wp()->utils->log( 'send_payment() request failed with error code ' . $code . ': ' . print_r( $body, true ) );

			return $request;

		} elseif ( 201 === $code && 'created' === strtolower( $message ) ) {

			$response = json_decode( $body, true );

			affiliate_wp()->utils->log( 'send_payment() request succeeded. Batch ID: ' . ( $response['batch_header']['payout_batch_id'] ?? 'unknown' ) );

			// Return the full response so the caller can extract the batch ID and handle payout creation and verification.
			return $response;

		} else {

			affiliate_wp()->utils->log( 'send_payment() request failed with error code ' . $code . ': ' . $message );
			affiliate_wp()->utils->log( 'send_payment() request args: ' . print_r( $args, true ) );
			affiliate_wp()->utils->log( 'send_payment() response body: ' . print_r( $body, true ) );

			// Try to decode the error response for more details.
			$decoded_body = json_decode( $body, true );
			if ( $decoded_body && isset( $decoded_body['message'] ) ) {
				affiliate_wp()->utils->log( 'PayPal API Error Details: ' . print_r( $decoded_body, true ) );
			}

			return new WP_Error( $code, $message );

		}
	}

	/**
	 * Process a referral payment for a bulk payout.
	 *
	 * Sends a bulk payout request to PayPal. Returns the decoded PayPal
	 * response on success (201 Created) so the caller can extract the
	 * batch ID and handle payout record creation and status verification.
	 *
	 * @access public
	 * @since 2.29.0
	 * @return array|WP_Error PayPal response array on success, WP_Error on failure.
	 */
	public function send_bulk_payment( $payouts = [] ) {

		$token = $this->get_token();

		if ( is_wp_error( $token ) ) {
			return $token;
		}

		$items = [];
		foreach ( $payouts as $affilate_id => $payout ) {

			/** This filter is documented in class-paypal-api.php send_payment(). */
			$note = apply_filters( 'affwp_paypal_payout_note', $payout['description'], [
				'email'        => $payout['email'],
				'amount'       => $payout['amount'],
				'description'  => $payout['description'],
				'affiliate_id' => (int) $affilate_id,
				'referral_id'  => 0,
			] );

			$items[] = [
				'recipient_type' => 'EMAIL',
				'amount'         => [
					'value'    => affwp_sanitize_amount( $payout['amount'] ),
					'currency' => affwp_get_currency(),
				],
				'receiver'       => $payout['email'],
				'note'           => $note,
				'sender_item_id' => (string) $affilate_id, // Cast to string for PayPal API.
			];

		}

		/*
		 * Batch-level subject: it covers the whole batch, not one affiliate, so
		 * the per-item context keys are present (matching send_payment()'s shape)
		 * but empty. This filter is documented in class-paypal-api.php send_payment().
		 */
		$email_subject = apply_filters( 'affwp_paypal_payout_email_subject', __( 'Affiliate Earnings Payout', 'affiliate-wp' ), array(
			'email'        => '',
			'amount'       => 0,
			'description'  => '',
			'affiliate_id' => 0,
			'referral_id'  => 0,
		) );

		$request = wp_remote_post(
			'https://api.' . $this->sandbox . 'paypal.com/v1/payments/payouts?sync_mode=false',
			[
				'headers'     => [
					'Content-Type'                  => 'application/json',
					'Authorization'                 => 'Bearer ' . $token->access_token,
					'PayPal-Partner-Attribution-Id' => 'EasyDigitalDownloads_SP',
				],
				'timeout'     => 45,
				'httpversion' => '1.1',
				'body'        => json_encode(
					[
						'sender_batch_header' => [
							'sender_batch_id' => md5( serialize( $items ) . gmdate( 'Y-m-d' ) ),
							'email_subject'   => $email_subject,
						],
						'items'               => $items,
					]
				),
			]
		);

		$body    = wp_remote_retrieve_body( $request );
		$code    = wp_remote_retrieve_response_code( $request );
		$message = wp_remote_retrieve_response_message( $request );

		if ( 201 === $code && 'created' === strtolower( $message ) ) {

			$response = json_decode( $body, true );

			affiliate_wp()->utils->log( 'send_bulk_payment() request succeeded. Batch ID: ' . ( $response['batch_header']['payout_batch_id'] ?? 'unknown' ) );

			return $response;

		} else {

			affiliate_wp()->utils->log( 'send_bulk_payment() request failed with error code ' . $code . ': ' . $message );
			affiliate_wp()->utils->log( 'send_payment() request items: ' . print_r( $items, true ) );
			affiliate_wp()->utils->log( 'send_payment() request attempt: ' . print_r( $request, true ) );

			$body = json_decode( $body );

			if ( ! empty( $body->name ) && 'VALIDATION_ERROR' === $body->name ) {

				$code    = $body->name;
				$message = $body->message . '. Details: ' . json_encode( $body->details ) . ' - ' . $body->information_link;

			}

			return new WP_Error( $code, $message );

		}
	}

	/**
	 * Test the PayPal API connection and verify Payouts permission.
	 *
	 * @access public
	 * @since 2.33.0
	 * @return array { status: 'success'|'warning'|'error', message: string }
	 */
	public function test_connection() {
		$token = $this->get_token();

		if ( is_wp_error( $token ) ) {
			$error = $this->get_friendly_error( $token );

			return [
				'status'  => 'error',
				'message' => $error->get_error_message(),
			];
		}

		/*
		 * Check the scope grants the Payouts permission. Scope is a space-separated list of URIs;
		 * match the whole-word "/payouts" segment (followed by whitespace or end of string) so a
		 * scope like "/payouts/read-only" doesn't produce a false positive.
		 */
		if ( isset( $token->scope ) && preg_match( '#/payouts(\s|$)#', $token->scope ) ) {
			return [
				'status'  => 'success',
				'message' => __( 'Connection successful! Your account is ready to process payouts.', 'affiliate-wp' ),
			];
		}

		// Connected but Payouts not enabled.
		$link = '<a href="https://developer.paypal.com/dashboard/applications" target="_blank" rel="noopener noreferrer" class="underline">developer.paypal.com</a>';

		return [
			'status'  => 'warning',
			/* translators: %s: link to PayPal developer dashboard */
			'message' => sprintf( __( 'Connected successfully, but Payouts is not enabled on your PayPal account. Enable it at %s under My Account > Payouts.', 'affiliate-wp' ), $link ),
		];
	}

	/**
	 * Map a PayPal API error to a user-friendly message.
	 *
	 * @access private
	 * @since 2.33.0
	 * @param WP_Error $error The original error from get_token().
	 * @return WP_Error A new WP_Error with a friendlier message, or the original if unmapped.
	 */
	private function get_friendly_error( $error ) {
		$link     = '<a href="https://developer.paypal.com/dashboard/applications" target="_blank" rel="noopener noreferrer" class="underline">developer.paypal.com</a>';
		$messages = [
			/* translators: %s: link to PayPal developer dashboard */
			'invalid_client' => sprintf( __( 'Client ID or Secret is incorrect. Double-check both values in your PayPal app at %s.', 'affiliate-wp' ), $link ),
			/* translators: %s: link to PayPal developer dashboard */
			'invalid_token'  => sprintf( __( 'Your credentials have expired or been revoked. Generate new credentials in your PayPal app at %s.', 'affiliate-wp' ), $link ),
		];

		$code = $error->get_error_code();

		if ( isset( $messages[ $code ] ) ) {
			return new WP_Error( $code, $messages[ $code ] );
		}

		return $error;
	}

	/**
	 * Retrieve the status of a payout batch from PayPal.
	 *
	 * Calls GET /v1/payments/payouts/{batch_id} to check the batch status
	 * and individual item transaction statuses. Used for async payout
	 * verification after the initial 201 Created response.
	 *
	 * @access public
	 * @since 2.33.0
	 * @param string $batch_id The PayPal payout batch ID.
	 * @return array|WP_Error Decoded response array on success, WP_Error on failure.
	 */
	public function get_batch_status( $batch_id ) {

		$token = $this->get_token();

		if ( is_wp_error( $token ) ) {
			return $token;
		}

		$request = wp_remote_get(
			'https://api.' . $this->sandbox . 'paypal.com/v1/payments/payouts/' . urlencode( $batch_id ),
			[
				'headers' => [
					'Content-Type'  => 'application/json',
					'Authorization' => 'Bearer ' . $token->access_token,
				],
				'timeout' => 45,
			]
		);

		$body    = wp_remote_retrieve_body( $request );
		$code    = wp_remote_retrieve_response_code( $request );
		$message = wp_remote_retrieve_response_message( $request );

		if ( is_wp_error( $request ) ) {

			affiliate_wp()->utils->log( 'get_batch_status() request failed: ' . print_r( $body, true ) );

			return $request;

		} elseif ( 200 === $code ) {

			return json_decode( $body, true );

		} else {

			affiliate_wp()->utils->log( 'get_batch_status() request failed with error code ' . $code . ': ' . $message );

			return new WP_Error( $code, $message );

		}
	}

	/**
	 * Retrieve an API access token
	 *
	 * @access private
	 * @since 2.29.0
	 * @return object|WP_Error
	 */
	private function get_token() {

		$request = wp_remote_post(
			'https://api.' . $this->sandbox . 'paypal.com/v1/oauth2/token',
			[
				'headers'     => [
					'Accept'          => 'application/json',
					'Accept-Language' => 'en_US',
					'Authorization'   => 'Basic ' . base64_encode( $this->credentials['client_id'] . ':' . $this->credentials['secret'] ),
				],
				'timeout'     => 45,
				'httpversion' => '1.1',
				'body'        => [
					'grant_type' => 'client_credentials',
				],
			]
		);

		$body    = wp_remote_retrieve_body( $request );
		$code    = wp_remote_retrieve_response_code( $request );
		$message = wp_remote_retrieve_response_message( $request );

		if ( is_wp_error( $request ) ) {

			affiliate_wp()->utils->log( 'get_token() request failed with error code ' . $code . ': ' . print_r( $body, true ) );

			return $request;

		} elseif ( 200 === $code && 'ok' === strtolower( $message ) ) {

			affiliate_wp()->utils->log( 'get_token() request succeeded: ' . print_r( $body, true ) );

			return json_decode( $body );

		} else {

			$body = json_decode( $body );

			if ( ! empty( $body->error ) ) {

				$code  = $body->error;
				$error = $body->error_description;

			} else {

				$code  = $code;
				$error = $message;

			}

			affiliate_wp()->utils->log( 'get_token() request failed with error code ' . $code . ': ' . $error );

			return new WP_Error( $code, $error );

		}
	}
}
