<?php

/**
 * PeachPay PayPal util class.
 *
 * @package PeachPay
 */

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

/**
 * PayPal API Client class.
 */
final class PeachPay_PayPal
{

	/**
	 * Zero decimal currencies.
	 *
	 * @var array
	 */
	const ZERO_DECIMAL_CURRENCIES = array(
		'BIF' => 1,
		'CLP' => 1,
		'DJF' => 1,
		'GNF' => 1,
		'JPY' => 1,
		'KMF' => 1,
		'KRW' => 1,
		'MGA' => 1,
		'PYG' => 1,
		'RWF' => 1,
		'UGX' => 1,
		'VND' => 1,
		'VUV' => 1,
		'XAF' => 1,
		'XOF' => 1,
		'XPF' => 1,
		'TWD' => 1,
		'HUF' => 1,
	);

	/**
	 * Formats an amount according to the associated currency.
	 *
	 * @param float|string $amount The amount to format.
	 * @param string       $currency_code The currency code.
	 */
	public static function format_amount($amount, $currency_code)
	{
		if (array_key_exists($currency_code, self::ZERO_DECIMAL_CURRENCIES)) {
			return number_format($amount, 0, '.', '');
		}

		return number_format($amount, 2, '.', '');
	}
	
	/**
	 * Builds a href for linking to a merchants PayPal dashboard.
	 *
	 * @param string  $mode A live or test mode url.
	 * @param string  $path The path of the URL.
	 * @param string  $id The specific resource we are looking for.
	 * @param boolean $echo To echo a href of the URL.
	 */
	public static function dashboard_url($mode, $path, $id, $echo = false)
	{

		$base_url = 'live' === $mode ? 'https://www.paypal.com' : 'https://www.sandbox.paypal.com';
		$url      = $base_url . '/' . $path . '/' . $id;

		if ($echo) {
			// PHPCS:ignore
			echo "<a href='$url' target='_blank'>$id</a>";
		}

		return $url;
	}

	/**
	 * Gets the PayPal REST API base url.
	 *
	 * @param string $mode live or test.
	 * @return string
	 */
	private static function api_base_url($mode)
	{
		return 'live' === $mode ? 'https://api.paypal.com' : 'https://api.sandbox.paypal.com';
	}

	/**
	 * Gets the configured PayPal client secret for direct API calls.
	 *
	 * @return string
	 */
	private static function client_secret()
	{
		if (! class_exists('PeachPay_PayPal_Credentials')) {
			return '';
		}

		return PeachPay_PayPal_Credentials::get_client_secret();
	}

	/**
	 * Gets a transient key for cached PayPal access tokens.
	 *
	 * @param string $mode live or test.
	 * @return string
	 */
	private static function access_token_cache_key($mode)
	{
		return 'peachpay_paypal_access_token_' . $mode;
	}

	/**
	 * Gets a PayPal REST API access token.
	 *
	 * @param string $mode live or test.
	 * @return array{success: bool, token?: string, message?: string}
	 */
	private static function get_access_token($mode)
	{
		$cache_key = self::access_token_cache_key($mode);
		$cached    = get_transient($cache_key);
		if (is_string($cached) && '' !== $cached) {
			return array(
				'success' => true,
				'token'   => $cached,
			);
		}

		$client_id     = PeachPay_PayPal_Integration::client_id();
		$client_secret = self::client_secret();
		if (empty($client_id) || empty($client_secret)) {
			return array(
				'success' => false,
				'message' => __('PayPal client credentials are incomplete.', 'peachpay-for-woocommerce'),
			);
		}

		$response = wp_remote_post(
			self::api_base_url($mode) . '/v1/oauth2/token',
			array(
				'headers' => array(
					'Content-Type'  => 'application/x-www-form-urlencoded',
					'Authorization' => 'Basic ' . base64_encode($client_id . ':' . $client_secret), // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
				),
				'body'    => 'grant_type=client_credentials',
			)
		);

		if (is_wp_error($response)) {
			return array(
				'success' => false,
				'message' => $response->get_error_message(),
			);
		}

		$body = json_decode(wp_remote_retrieve_body($response), true);
		$code = (int) wp_remote_retrieve_response_code($response);
		if (200 !== $code || ! is_array($body) || empty($body['access_token'])) {
			$message = is_array($body) && ! empty($body['error_description'])
				? $body['error_description']
				: __('Could not authenticate with PayPal.', 'peachpay-for-woocommerce');

			return array(
				'success' => false,
				'message' => $message,
			);
		}

		$expires_in = isset($body['expires_in']) ? max(60, intval($body['expires_in']) - 60) : HOUR_IN_SECONDS;
		set_transient($cache_key, $body['access_token'], $expires_in);

		return array(
			'success' => true,
			'token'   => $body['access_token'],
		);
	}

	/**
	 * Parses a PayPal REST API error payload into a readable message.
	 *
	 * @param mixed  $body_response Response body.
	 * @param string $fallback Default message.
	 * @return string
	 */
	private static function api_error_message($body_response, $fallback)
	{
		if (is_array($body_response)) {
			if (! empty($body_response['message'])) {
				return $body_response['message'];
			}

			if (! empty($body_response['details'][0]['description'])) {
				return $body_response['details'][0]['description'];
			}

			if (! empty($body_response['error_description'])) {
				return $body_response['error_description'];
			}
		}

		if (is_string($body_response) && '' !== trim($body_response)) {
			return trim($body_response);
		}

		return $fallback;
	}

	/**
	 * Builds a refund-specific PayPal idempotency key.
	 *
	 * Uses the current number of WooCommerce refunds + 1 as the next refund attempt
	 * index. This keeps retries for the same in-flight refund stable (same index),
	 * while allowing subsequent partial refunds to use a distinct key.
	 *
	 * @param WC_Order $order WooCommerce order.
	 * @param string   $paypal_capture_id PayPal capture id.
	 * @param array    $refund_params PayPal refund request params.
	 * @return string
	 */
	private static function build_refund_request_id($order, $paypal_capture_id, $refund_params)
	{
		$attempt_index = count($order->get_refunds()) + 1;
		$amount        = isset($refund_params['amount']['value']) ? strval($refund_params['amount']['value']) : '';
		$currency      = isset($refund_params['amount']['currency_code']) ? strval($refund_params['amount']['currency_code']) : '';
		$reason        = isset($refund_params['note_to_payer']) ? strval($refund_params['note_to_payer']) : '';

		// Keep header compact and deterministic while avoiding unbounded length.
		$fingerprint = md5(
			implode(
				'|',
				array(
					$order->get_id(),
					$paypal_capture_id,
					$attempt_index,
					$amount,
					$currency,
					$reason,
				)
			)
		);

		return sprintf('pp-refund-%d-%d-%s', $order->get_id(), $attempt_index, $fingerprint);
	}

	/**
	 * Sends a direct request to the PayPal REST API.
	 *
	 * @param string     $method HTTP method.
	 * @param string     $path API path.
	 * @param string     $mode live or test.
	 * @param array|null $body Optional JSON body.
	 * @param array      $headers Optional extra headers.
	 * @return array{success: bool, data?: mixed, message?: string, status_code?: int}
	 */
	private static function api_request($method, $path, $mode, $body = null, $headers = array())
	{
		$token_result = self::get_access_token($mode);
		if (! $token_result['success']) {
			return array(
				'success' => false,
				'message' => $token_result['message'],
			);
		}

		$request_headers = array_merge(
			array(
				'Authorization' => 'Bearer ' . $token_result['token'],
				'Accept'        => 'application/json',
				'Content-Type'  => 'application/json',
			),
			$headers
		);

		$args = array(
			'method'  => $method,
			'headers' => $request_headers,
		);

		if (null !== $body) {
			$args['body'] = wp_json_encode($body);
		}

		$response = wp_remote_request(self::api_base_url($mode) . $path, $args);
		if (is_wp_error($response)) {
			return array(
				'success' => false,
				'message' => $response->get_error_message(),
			);
		}

		$status_code = (int) wp_remote_retrieve_response_code($response);
		$raw_body    = wp_remote_retrieve_body($response);
		$decoded     = '' !== $raw_body ? json_decode($raw_body, true) : null;

		if ($status_code < 200 || $status_code >= 300) {
			return array(
				'success'     => false,
				'status_code' => $status_code,
				'message'     => self::api_error_message($decoded ? $decoded : $raw_body, __('PayPal API request failed.', 'peachpay-for-woocommerce')),
			);
		}

		if (204 === $status_code) {
			return array(
				'success'     => true,
				'status_code' => $status_code,
				'data'        => null,
			);
		}

		if (! is_array($decoded)) {
			return array(
				'success'     => false,
				'status_code' => $status_code,
				'message'     => __('PayPal API returned an invalid response.', 'peachpay-for-woocommerce'),
			);
		}

		return array(
			'success'     => true,
			'status_code' => $status_code,
			'data'        => $decoded,
		);
	}

	/**
	 * Fetches the latest PayPal order details and syncs them to the WooCommerce order.
	 *
	 * @param WC_Order $order WooCommerce order.
	 * @param string   $mode live or test.
	 * @param string   $paypal_order_id PayPal order id.
	 * @return array{success: bool, data?: array, message?: string}
	 */
	private static function fetch_order_details($order, $mode, $paypal_order_id)
	{
		$result = self::api_request('GET', '/v2/checkout/orders/' . rawurlencode($paypal_order_id), $mode);
		if (! $result['success']) {
			return $result;
		}

		self::calculate_payment_state($order, $result['data']);
		return $result;
	}

	/**
	 * Removes direct-integration-incompatible fields from PayPal order payloads.
	 *
	 * @param array $paypal_order_param Original payload.
	 * @return array
	 */
	private static function sanitize_order_params($paypal_order_param)
	{
		if (empty($paypal_order_param['purchase_units']) || ! is_array($paypal_order_param['purchase_units'])) {
			return $paypal_order_param;
		}

		foreach ($paypal_order_param['purchase_units'] as &$purchase_unit) {
			if (empty($purchase_unit['payee']['merchant_id'])) {
				unset($purchase_unit['payee']);
			}

			$platform_fees = $purchase_unit['payment_instruction']['platform_fees'] ?? array();
			$has_non_zero_platform_fee = false;
			foreach ($platform_fees as $platform_fee) {
				$value = isset($platform_fee['amount']['value']) ? (float) $platform_fee['amount']['value'] : 0;
				if ($value > 0) {
					$has_non_zero_platform_fee = true;
					break;
				}
			}

			if (! $has_non_zero_platform_fee) {
				unset($purchase_unit['payment_instruction']);
			}
		}

		return $paypal_order_param;
	}

	/**
	 * Creates a order in PayPal.
	 *
	 * @param WC_Order $order The woocommerce order to create a payment intent for.
	 * @param array    $paypal_order_param The parameters for the payment intent.
	 * @param array    $order_details The order details needed to create the payment intent.
	 * @param string   $mode The mode to place the payment in.
	 */
	public static function create_order($order, $paypal_order_param, $order_details, $mode)
	{
		peachpay_log(
			'debug',
			'PayPal create_order: request start',
			array(
				'order_id'           => $order->get_id(),
				'order_number'       => $order->get_order_number(),
				'mode'               => $mode,
				'currency'           => $order->get_currency(),
				'total'              => $order->get_total(),
				'has_transaction_id' => ! empty(PeachPay_PayPal_Order_Data::get_peachpay($order, 'transaction_id')),
				'has_session_id'     => ! empty(PeachPay_PayPal_Order_Data::get_peachpay($order, 'session_id')),
				'has_merchant_id'    => ! empty(PeachPay_PayPal_Integration::merchant_id()),
			)
		);

		$sanitized_payload = self::sanitize_order_params($paypal_order_param);
		$request           = self::api_request(
			'POST',
			'/v2/checkout/orders',
			$mode,
			$sanitized_payload,
			array(
				'Prefer'            => 'return=representation',
				'PayPal-Request-Id' => PeachPay_PayPal_Order_Data::get_peachpay($order, 'transaction_id'),
			)
		);

		if (! $request['success']) {
			$order->update_status(
				'failed',
				sprintf(
					// translators: The failure reason
					__(
						'PayPal API request failed:%s',
						'peachpay-for-woocommerce'
					),
					$request['message']
				)
			);

			return null;
		}

		$data = $request['data'];
		peachpay_log(
			'info',
			'PayPal create_order: success',
			array(
				'order_id'        => $order->get_id(),
				'paypal_order_id' => isset($data['id']) ? $data['id'] : null,
				'status'          => isset($data['status']) ? $data['status'] : null,
			)
		);
		self::calculate_payment_state($order, $data);

		return $data;
	}

	/**
	 * Updates the PayPal order.
	 *
	 * @param WC_Order $order The WC order to update a PayPal order with.
	 */
	public static function update_order($order)
	{
		$paypal_mode     = PeachPay_PayPal_Order_Data::get_peachpay($order, 'paypal_mode');
		$paypal_order_id = PeachPay_PayPal_Order_Data::get_order_transaction_details($order, 'id');

		$value =
			array(
				'reference_id' => $order->get_order_number(),
				'description'  => PeachPay_PayPal_Payment_Gateway::get_payment_description($order),
				'payee'        => array(
					'merchant_id' => PeachPay_PayPal_Integration::merchant_id(),
				),
				'amount'       => array(
					'currency_code' => $order->get_currency(),
					'value'         => self::format_amount($order->get_total(), $order->get_currency()),
					'breakdown'     => array(
						'discount'   => array(
							'currency_code' => $order->get_currency(),
							'value'         => self::format_amount($order->get_discount_total(), $order->get_currency()),
						),
						'item_total' => array(
							'currency_code' => $order->get_currency(),
							'value'         => self::format_amount($order->get_subtotal(), $order->get_currency()),
						),
						'handling'   => array(
							'currency_code' => $order->get_currency(),
							'value'         => self::format_amount($order->get_total_fees(), $order->get_currency()),
						),
						'shipping'   => array(
							'currency_code' => $order->get_currency(),
							'value'         => self::format_amount($order->get_shipping_total(), $order->get_currency()),
						),
						'tax_total'  => array(
							'currency_code' => $order->get_currency(),
							'value'         => self::format_amount($order->get_total_tax(), $order->get_currency()),
						),
					),
				),
			);

		if (PeachPay_PayPal_Advanced::get_setting('itemized_order_details') === 'yes') {
			$value['items'] = PeachPay_PayPal_Payment_Gateway::get_paypal_order_line_items($order);
		}

		$patch_body = array(
			array(
				'op'    => 'replace',
				'path'  => "/purchase_units/@reference_id=='" . $order->get_order_number() . "'",
				'value' => $value,
			),
		);

		$result = self::api_request(
			'PATCH',
			'/v2/checkout/orders/' . rawurlencode($paypal_order_id),
			$paypal_mode,
			$patch_body,
			array(
				'Prefer'            => 'return=representation',
				'PayPal-Request-Id' => PeachPay_PayPal_Order_Data::get_peachpay($order, 'transaction_id'),
			)
		);

		if (! $result['success']) {
			return array(
				'success' => false,
				'message' => $result['message'],
			);
		}

		self::calculate_payment_state($order, $result['data']);

		return $result['data'];
	}

	/**
	 * Captures the PayPal order.
	 *
	 * @param WC_Order $order The WC order to capture a PayPal order for.
	 */
	public static function capture_order($order)
	{
		$paypal_mode     = PeachPay_PayPal_Order_Data::get_peachpay($order, 'paypal_mode');
		$paypal_order_id = PeachPay_PayPal_Order_Data::get_order_transaction_details($order, 'id');

		$request = self::api_request(
			'POST',
			'/v2/checkout/orders/' . rawurlencode($paypal_order_id) . '/capture',
			$paypal_mode,
			null,
			array(
				'Prefer'            => 'return=representation',
				'PayPal-Request-Id' => PeachPay_PayPal_Order_Data::get_peachpay($order, 'transaction_id'),
			)
		);

		if (! $request['success']) {
			return array(
				'success' => false,
				'message' => $request['message'],
			);
		}

		$data = $request['data'];
		self::calculate_payment_state($order, $data);

		return array(
			'success' => true,
			'data'    => $data,
		);
	}

	/**
	 * Refunds a PayPal order.
	 *
	 * @param WC_Order $order The WC order to refund a PayPal payment for.
	 * @param array    $refund_params The parameters to refund the PayPal payment with.
	 */
	public static function refund_payment($order, $refund_params)
	{
		$paypal_mode       = PeachPay_PayPal_Order_Data::get_peachpay($order, 'paypal_mode');
		$paypal_order_id   = PeachPay_PayPal_Order_Data::get_order_transaction_details($order, 'id');
		$paypal_capture_id = $order->get_transaction_id();
		$request_id        = self::build_refund_request_id($order, $paypal_capture_id, $refund_params);

		$result = self::api_request(
			'POST',
			'/v2/payments/captures/' . rawurlencode($paypal_capture_id) . '/refund',
			$paypal_mode,
			$refund_params,
			array(
				'Prefer'            => 'return=representation',
				'PayPal-Request-Id' => $request_id,
			)
		);

		if (! $result['success']) {
			return array(
				'success' => false,
				'message' => $result['message'],
			);
		}

		$sync = self::fetch_order_details($order, $paypal_mode, $paypal_order_id);
		if (! $sync['success']) {
			peachpay_log(
				'warning',
				'PayPal refund succeeded but order refresh failed',
				array(
					'order_id' => $order->get_id(),
					'message'  => $sync['message'] ?? '',
				)
			);
		}

		$json = array(
			'success' => true,
			'data'    => $sync['success'] ? $sync['data'] : array( 'id' => $paypal_order_id ),
		);
		// fetch_order_details() already called calculate_payment_state when sync succeeded.

		$refunds = PeachPay_PayPal_Order_Data::get_refunded_payments($order);

		if (is_array($refunds) && count($refunds) > 0) {
			$refund = array_pop($refunds);

			$refund_id     = $refund['id'];
			$refund_amount = wc_price($refund['amount']['value'], array('currency' => $refund['amount']['currency_code']));

			// translators: % 1$s the payment method title, % 3$s Refund amount, % 4$s Refund Id .
			$order->add_order_note(sprintf(__('%1$s payment refunded %2$s (Refund Id: %3$s)', 'peachpay-for-woocommerce'), $order->get_payment_method_title(), $refund_amount, $refund_id));
		}

		return $json;
	}

	/**
	 * Calculates the WC order state for based on the current state of the PayPal transaction.
	 *
	 * @param WC_Order $order The WC order to update.
	 * @param array    $payment_details The PayPal payment details to use to update the order.
	 */
	public static function calculate_payment_state($order, $payment_details = null)
	{
		if (null !== $payment_details) {
			PeachPay_PayPal_Order_Data::set_order_transaction_details($order, $payment_details);
		}

		$paypal_order_status = PeachPay_PayPal_Order_Data::get_order_transaction_details($order, 'status');
		$paypal_order_id     = PeachPay_PayPal_Order_Data::get_order_transaction_details($order, 'id');

		$paypal_transaction_id = null;
		$purchase_units        = PeachPay_PayPal_Order_Data::get_order_transaction_details($order, 'purchase_units');
		if (is_array($purchase_units) && 0 < count($purchase_units) && isset($purchase_units[0]['payments'])) {
			$payments = $purchase_units[0]['payments'];

			if (isset($payments['captures']) && is_array($payments['captures']) && 0 < count($payments['captures'])) {
				$capture = $payments['captures'][0];

				$paypal_transaction_id = $capture['id'];
			}
		}

		if ('CREATED' === $paypal_order_status || 'SAVED' === $paypal_order_status) {
			// translators: %1$s Payment method, %2$s transaction id.
			$order->add_order_note(sprintf(__('%1$s payment was created. Awaiting customer confirmation. (PayPal Order Id: %2$s)', 'peachpay-for-woocommerce'), $order->get_payment_method_title(), $paypal_order_id));
		} elseif ('APPROVED' === $paypal_order_status) {
			// translators: %1$s Payment method, %2$s transaction id.
			$order->set_status('on-hold', sprintf(__('%1$s payment was approved. Awaiting capture. (PayPal Order Id: %2$s)', 'peachpay-for-woocommerce'), $order->get_payment_method_title(), $paypal_order_id));
		} elseif ('VOIDED' === $paypal_order_status) {
			// translators: %1$s Payment method, %2$s transaction id.
			$order->set_status('cancelled', sprintf(__('%1$s payment was voided. (PayPal Order Id: %2$s)', 'peachpay-for-woocommerce'), $order->get_payment_method_title(), $paypal_order_id));
		} elseif ('COMPLETED' === $paypal_order_status && ! $order->is_paid() && ! $order->get_transaction_id()) {

			if ($paypal_transaction_id) {
				$order->payment_complete($paypal_transaction_id);
				// translators: %1$s Payment method, %2$s transaction id.
				$order->add_order_note(sprintf(__('%1$s payment is complete. (Capture Id: %2$s)', 'peachpay-for-woocommerce'), $order->get_payment_method_title(), $paypal_transaction_id));
			}
		}

		$order->save();
	}
}
