<?php

namespace DigitalApps\PaymentsForAdyen;

defined('ABSPATH') || exit;

require_once DIGITALAPPS_PAYMENTS_FOR_ADYEN_PATH . 'includes/logger.php';

/** Checkout API client shared by the Free Card gateway and Pro add-on. */
class Adyen_Client
{
	const MAX_ATTEMPTS = 3;

	private $api_key;
	private $merchant_account;
	private $test_mode;
	private $capture_mode;
	private $endpoint;
	private $logger;

	/**
	 * @param array|null $settings Optional settings override for contract tests.
	 */
	public function __construct($settings = null)
	{
		$settings = is_array($settings) ? $settings : Settings_Store::get();
		$environment = Settings_Store::get_environment(null, $settings);
		$this->api_key = (string) ($environment['apiKey'] ?? '');
		$this->merchant_account = (string) ($environment['merchantAccount'] ?? '');
		$this->test_mode = !empty($environment['testMode']);
		$this->capture_mode = 'manual' === ($settings['captureMode'] ?? '') ? 'manual' : 'automatic';
		$this->logger = new Logger();

		try {
			$this->endpoint = Adyen_Endpoint_Resolver::checkout_base_url(
				$this->test_mode,
				(string) ($environment['liveUrlPrefix'] ?? '')
			);
		} catch (\InvalidArgumentException $exception) {
			$this->endpoint = '';
			$this->logger->log('ERROR', ['message' => 'Adyen endpoint configuration is invalid']);
		}
	}

	/**
	 * Build a client for the immutable environment recorded on an existing order.
	 *
	 * @param \WC_Order $order WooCommerce order.
	 * @return self
	 */
	public static function for_order($order)
	{
		$settings = Settings_Store::get();
		$environment = $order && is_callable([$order, 'get_meta'])
			? (string) $order->get_meta('_digitalapps_payments_for_adyen_environment', true)
			: '';
		if (in_array($environment, ['test', 'live'], true)) {
			$settings['testMode'] = 'test' === $environment;
		}

		return new self($settings);
	}

	/** Return the merchant reference for an order. */
	private function get_merchant_reference($order)
	{
		$default_reference = (string) $order->get_id();
		$reference = apply_filters('digitalapps_payments_for_adyen_merchant_reference', $default_reference, $order);

		return is_scalar($reference) && '' !== trim((string) $reference)
			? substr((string) $reference, 0, 80)
			: $default_reference;
	}

	/**
	 * Create a payment or authorization.
	 *
	 * @param \WC_Order   $order WooCommerce order.
	 * @param string|array $payment_method_or_options Payment method type or request options.
	 * @return array
	 */
	public function create_payment($order, $payment_method_or_options = 'scheme')
	{
		if (is_string($payment_method_or_options)) {
			$payment_method = ['type' => $payment_method_or_options];
			$options = [];
		} elseif (is_array($payment_method_or_options)) {
			$options = $payment_method_or_options;
			$payment_method = $options['paymentMethod'] ?? ['type' => 'scheme'];
		} else {
			$options = [];
			$payment_method = ['type' => 'scheme'];
		}

		$remote_address = isset($_SERVER['REMOTE_ADDR'])
			? sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR']))
			: '127.0.0.1';
		$browser_info = $options['browserInfo'] ?? ($payment_method['browserInfo'] ?? null);
		unset($payment_method['browserInfo']);

		$merchant_reference = $this->get_merchant_reference($order);
		/**
		 * Filter the browser 3DS2 authentication mode for a Card payment.
		 *
		 * The default prefers native authentication. Return `disabled` to force
		 * Adyen's redirect authentication flow for a trusted integration test.
		 *
		 * @since 1.0.5
		 *
		 * @param string    $native_three_ds Native 3DS2 mode: `preferred` or `disabled`.
		 * @param \WC_Order $order            WooCommerce order being paid.
		 */
		$native_three_ds = apply_filters('digitalapps_payments_for_adyen_native_three_ds', 'preferred', $order);
		if (!in_array($native_three_ds, ['preferred', 'disabled'], true)) {
			$native_three_ds = 'preferred';
		}
		$order->update_meta_data('_digitalapps_payments_for_adyen_merchant_reference', $merchant_reference);
		$order->update_meta_data('_digitalapps_payments_for_adyen_environment', $this->test_mode ? 'test' : 'live');
		$order->update_meta_data('_digitalapps_payments_for_adyen_capture_mode', $this->capture_mode);
		$order->save();

		$body = [
			'amount' => [
				'value' => Adyen_Amount::to_minor_units($order->get_total(), $order->get_currency()),
				'currency' => $order->get_currency(),
			],
			'reference' => $merchant_reference,
			'merchantAccount' => $this->merchant_account,
			'paymentMethod' => $payment_method,
			'returnUrl' => $options['returnUrl'] ?? $order->get_checkout_order_received_url(),
			'shopperEmail' => $order->get_billing_email(),
			'shopperReference' => $this->get_shopper_reference($order),
			'shopperIP' => $order->get_customer_ip_address() ?: $remote_address,
			'countryCode' => $order->get_billing_country() ?: 'US',
			'shopperLocale' => str_replace('_', '-', determine_locale()),
			'channel' => 'Web',
			'origin' => home_url(),
			'billingAddress' => [
				'street' => $order->get_billing_address_1(),
				'houseNumberOrName' => $order->get_billing_address_2() ?: 'N/A',
				'city' => $order->get_billing_city(),
				'postalCode' => $order->get_billing_postcode(),
				'stateOrProvince' => $order->get_billing_state(),
				'country' => $order->get_billing_country() ?: 'US',
			],
			'shopperName' => [
				'firstName' => $order->get_billing_first_name(),
				'lastName' => $order->get_billing_last_name(),
			],
			'authenticationData' => [
				'threeDSRequestData' => ['nativeThreeDS' => $native_three_ds],
			],
		];

		if ('manual' === $this->capture_mode) {
			$body['additionalData'] = ['manualCapture' => 'true'];
		}
		if (is_array($browser_info)) {
			$body['browserInfo'] = $browser_info;
		}
		if (!empty($options['sessionId'])) {
			$body['sessionId'] = $options['sessionId'];
		}
		if ($order->needs_shipping_address()) {
			$body['deliveryAddress'] = [
				'street' => $order->get_shipping_address_1() ?: $order->get_billing_address_1(),
				'houseNumberOrName' => $order->get_shipping_address_2() ?: $order->get_billing_address_2() ?: 'N/A',
				'city' => $order->get_shipping_city() ?: $order->get_billing_city(),
				'postalCode' => $order->get_shipping_postcode() ?: $order->get_billing_postcode(),
				'stateOrProvince' => $order->get_shipping_state() ?: $order->get_billing_state(),
				'country' => $order->get_shipping_country() ?: $order->get_billing_country() ?: 'US',
			];
		}

		return $this->post(
			'/payments',
			$body,
			Adyen_Operation::key_for_order($order, 'payment:create'),
			15
		);
	}

	/** Initiate a capture. */
	public function capture($psp_reference, $amount, $currency, $operation_id = '')
	{
		if (!is_numeric($amount) || (float) $amount <= 0) {
			return $this->local_error('invalid_amount', __('Invalid amount for capture.', 'digital-apps-payments-for-adyen'));
		}
		$operation_id = $operation_id ?: 'capture:' . $psp_reference . ':' . $currency . ':' . $amount;
		$body = [
			'amount' => [
				'value' => Adyen_Amount::to_minor_units($amount, $currency),
				'currency' => $currency,
			],
			'merchantAccount' => $this->merchant_account,
			'reference' => $this->operation_reference('Capture', $operation_id),
		];

		return $this->post(
			'/payments/' . rawurlencode($psp_reference) . '/captures',
			$body,
			Adyen_Operation::deterministic_key($operation_id),
			30
		);
	}

	/** Initiate a full or partial refund. */
	public function refund($psp_reference, $amount, $currency, $reason = '', $operation_id = '')
	{
		if (!is_numeric($amount) || (float) $amount <= 0) {
			return $this->local_error('invalid_amount', __('Invalid amount for refund.', 'digital-apps-payments-for-adyen'));
		}
		$operation_id = $operation_id ?: 'refund:' . $psp_reference . ':' . $currency . ':' . $amount;
		$body = [
			'amount' => [
				'value' => Adyen_Amount::to_minor_units($amount, $currency),
				'currency' => $currency,
			],
			'merchantAccount' => $this->merchant_account,
			'reference' => $this->operation_reference('Refund', $operation_id),
			'merchantRefundReason' => self::map_refund_reason($reason),
		];

		return $this->post(
			'/payments/' . rawurlencode($psp_reference) . '/refunds',
			$body,
			Adyen_Operation::deterministic_key($operation_id),
			30
		);
	}

	/** Initiate cancellation of an uncaptured payment. */
	public function cancel($psp_reference, $operation_id = '')
	{
		$operation_id = $operation_id ?: 'cancel:' . $psp_reference;
		return $this->post(
			'/payments/' . rawurlencode($psp_reference) . '/cancels',
			[
				'merchantAccount' => $this->merchant_account,
				'reference' => $this->operation_reference('Cancel', $operation_id),
			],
			Adyen_Operation::deterministic_key($operation_id),
			30
		);
	}

	/** Initiate a race-safe cancel-or-refund reversal. */
	public function reverse($psp_reference, $operation_id = '')
	{
		$operation_id = $operation_id ?: 'reversal:' . $psp_reference;
		return $this->post(
			'/payments/' . rawurlencode($psp_reference) . '/reversals',
			[
				'merchantAccount' => $this->merchant_account,
				'reference' => $this->operation_reference('Reversal', $operation_id),
			],
			Adyen_Operation::deterministic_key($operation_id),
			30
		);
	}

	/** Get available payment methods from Adyen. */
	public function get_payment_methods()
	{
		$result = $this->get_payment_methods_result();
		return !empty($result['success']) ? $result['paymentMethods'] : [];
	}

	/** Return payment methods without collapsing connection errors into absence. */
	public function get_payment_methods_result()
	{
		$base_location = function_exists('wc_get_base_location') ? wc_get_base_location() : [];
		$country_code = $base_location['country'] ?? 'AU';
		$currency = function_exists('get_woocommerce_currency') ? get_woocommerce_currency() : 'AUD';
		$body = [
			'merchantAccount' => $this->merchant_account,
			'countryCode' => $country_code,
			'amount' => ['currency' => $currency, 'value' => 100],
			'channel' => 'Web',
		];
		$key_scope = 'payment-methods:' . $this->merchant_account . ':' . $country_code . ':' . $currency;
		$data = $this->post('/paymentMethods', $body, Adyen_Operation::deterministic_key($key_scope), 30);

		if (!empty($data['error'])) {
			return [
				'success' => false,
				'paymentMethods' => [],
				'message' => __('Adyen could not verify the selected environment credentials and merchant account.', 'digital-apps-payments-for-adyen'),
				'statusCode' => (int) ($data['httpStatus'] ?? 0),
			];
		}

		return [
			'success' => true,
			'paymentMethods' => isset($data['paymentMethods']) && is_array($data['paymentMethods']) ? $data['paymentMethods'] : [],
			'message' => '',
			'statusCode' => (int) ($data['httpStatus'] ?? 200),
		];
	}

	/** Create an Adyen payment session for shared callers. */
	public function create_session($order)
	{
		$body = [
			'merchantAccount' => $this->merchant_account,
			'amount' => [
				'value' => Adyen_Amount::to_minor_units($order->get_total(), $order->get_currency()),
				'currency' => $order->get_currency(),
			],
			'reference' => $this->get_merchant_reference($order),
			'countryCode' => $order->get_billing_country() ?: 'AU',
			'shopperLocale' => str_replace('_', '-', determine_locale()),
			'returnUrl' => $order->get_checkout_order_received_url(),
		];

		$data = $this->post('/sessions', $body, Adyen_Operation::key_for_order($order, 'session:create'), 15);
		return empty($data['error']) ? $data : null;
	}

	/** Submit action details to the Advanced flow. */
	public function submit_additional_details($payment_data, $details, $operation_id = '')
	{
		$operation_id = $operation_id ?: 'details:' . hash('sha256', wp_json_encode($details));
		$body = ['details' => $details];
		if (is_string($payment_data) && '' !== $payment_data) {
			$body['paymentData'] = $payment_data;
		}

		return $this->post(
			'/payments/details',
			$body,
			Adyen_Operation::deterministic_key($operation_id),
			15
		);
	}

	/** Map arbitrary WooCommerce reasons to Adyen's supported enum. */
	public static function map_refund_reason($reason)
	{
		$normalized = strtolower(trim((string) $reason));
		if (false !== strpos($normalized, 'fraud')) {
			return 'FRAUD';
		}
		if (false !== strpos($normalized, 'duplicate')) {
			return 'DUPLICATE';
		}
		if (false !== strpos($normalized, 'return')) {
			return 'RETURN';
		}
		if (false !== strpos($normalized, 'customer') || false !== strpos($normalized, 'shopper')) {
			return 'CUSTOMER REQUEST';
		}
		return 'OTHER';
	}

	/**
	 * Send a schema-supported Checkout API POST with a stable idempotency key.
	 *
	 * @param string $path Endpoint path.
	 * @param array  $body Request body.
	 * @param string $idempotency_key Stable key for this logical operation.
	 * @param int    $timeout Timeout seconds.
	 * @return array
	 */
	private function post($path, array $body, $idempotency_key, $timeout)
	{
		if ('' === $this->endpoint || '' === $this->api_key || '' === $this->merchant_account) {
			return $this->local_error('configuration_error', __('Adyen is not configured for the selected environment.', 'digital-apps-payments-for-adyen'));
		}

		$body = Adyen_Application_Info::add_to($body);
		$url = $this->endpoint . $path;
		$headers = [
			'Content-Type' => 'application/json',
			'X-API-Key' => $this->api_key,
			'Idempotency-Key' => substr((string) $idempotency_key, 0, 64),
		];

		for ($attempt = 1; $attempt <= self::MAX_ATTEMPTS; $attempt++) {
			$started = microtime(true);
			$response = wp_remote_post($url, [
				'headers' => $headers,
				'body' => wp_json_encode($body),
				'timeout' => $timeout,
			]);
			$this->logger->log('INFO', [
				'message' => 'Adyen Checkout API request completed',
				'operation' => $path,
				'attempt' => $attempt,
				'duration_ms' => round((microtime(true) - $started) * 1000, 2),
			]);

			if (is_wp_error($response)) {
				if ($attempt < self::MAX_ATTEMPTS) {
					$this->backoff($attempt, 0);
					continue;
				}
				$this->logger->log('ERROR', [
					'message' => 'Adyen Checkout API transport failure',
					'operation' => $path,
					'error_code' => $response->get_error_code(),
				]);
				return $this->local_error($response->get_error_code(), $response->get_error_message());
			}

			$status_code = (int) wp_remote_retrieve_response_code($response);
			$transient = 'true' === strtolower((string) wp_remote_retrieve_header($response, 'transient-error'));
			if ($attempt < self::MAX_ATTEMPTS && $this->is_retryable($status_code, $transient)) {
				$this->backoff($attempt, $status_code);
				continue;
			}

			$data = json_decode(wp_remote_retrieve_body($response), true);
			if ($status_code < 200 || $status_code >= 300 || !is_array($data)) {
				$this->logger->log('ERROR', [
					'message' => 'Adyen Checkout API request rejected',
					'operation' => $path,
					'http_status' => $status_code,
					'error_code' => is_array($data) ? ($data['errorCode'] ?? '') : 'invalid_json',
				]);
				return [
					'error' => true,
					'errorCode' => is_array($data) ? ($data['errorCode'] ?? 'http_' . $status_code) : 'invalid_json',
					'message' => is_array($data) ? ($data['message'] ?? 'Checkout API request failed') : 'Invalid Checkout API response',
					'errorType' => is_array($data) ? ($data['errorType'] ?? 'HTTP_ERROR') : 'INVALID_RESPONSE',
					'httpStatus' => $status_code,
					'resultCode' => 'Error',
				];
			}

			$data['httpStatus'] = $status_code;
			return $data;
		}

		return $this->local_error('retry_exhausted', __('Adyen request retry limit reached.', 'digital-apps-payments-for-adyen'));
	}

	private function is_retryable($status_code, $transient)
	{
		if (408 === $status_code || 429 === $status_code) {
			return true;
		}
		return $transient && in_array($status_code, [409, 422, 500, 502, 503, 504], true);
	}

	private function backoff($attempt, $status_code)
	{
		$delay_ms = (250 * (2 ** ($attempt - 1))) + wp_rand(0, 150);
		$delay_ms = (int) apply_filters('digitalapps_payments_for_adyen_retry_delay_ms', $delay_ms, $attempt, $status_code);
		if ($delay_ms > 0) {
			usleep(min($delay_ms, 5000) * 1000);
		}
	}

	private function operation_reference($prefix, $operation_id)
	{
		$suffix = substr(hash('sha256', (string) $operation_id), 0, 24);
		return substr($prefix . '-' . $suffix, 0, 80);
	}

	private function local_error($code, $message)
	{
		return [
			'error' => true,
			'errorCode' => sanitize_key((string) $code),
			'message' => (string) $message,
			'resultCode' => 'Error',
			'httpStatus' => 0,
		];
	}

	private function get_shopper_reference($order)
	{
		$customer_id = $order->get_customer_id();
		$reference = $customer_id ? 'customer-' . $customer_id : 'guest-order-' . $order->get_id();
		return substr($reference, 0, 80);
	}
}
