<?php

namespace DigitalApps\PaymentsForAdyen\Api;

defined('ABSPATH') || exit;

use Adyen\Util\HmacSignature;
use DigitalApps\PaymentsForAdyen\Adyen_Amount;
use DigitalApps\PaymentsForAdyen\Adyen_Operation;
use DigitalApps\PaymentsForAdyen\Adyen_Webhook_Inbox;
use DigitalApps\PaymentsForAdyen\Logger;
use DigitalApps\PaymentsForAdyen\Settings_Store;
use WP_REST_Controller;
use WP_REST_Response;
use WP_REST_Server;

/** Verify and durably ingest Adyen Standard webhook notifications. */
class Webhook_Controller extends WP_REST_Controller
{
	const EVENT_DATES_META_KEY = '_digitalapps_payments_for_adyen_webhook_event_dates';
	const CONFIRMED_REFUNDS_META_KEY = '_digitalapps_payments_for_adyen_confirmed_refunds';

	protected $namespace = 'digitalapps-payments-for-adyen/v1';
	protected $rest_base = 'webhook/(?P<token>[a-zA-Z0-9_-]+)';

	public function register_routes()
	{
		register_rest_route($this->namespace, '/' . $this->rest_base, [
			'methods' => WP_REST_Server::CREATABLE,
			'callback' => [$this, 'handle_notification'],
			'permission_callback' => [$this, 'verify_request'],
		]);
	}

	/** Validate endpoint token, optional Basic Auth, environment, and item HMAC. */
	public function verify_request($request)
	{
		$settings = Settings_Store::get();
		$expected_token = (string) ($settings['webhookToken'] ?? '');
		$token = $request->get_param('token');
		if ('' === $expected_token || !is_string($token) || !hash_equals($expected_token, $token)) {
			return new \WP_Error('invalid_token', __('Invalid webhook token', 'digital-apps-payments-for-adyen'), ['status' => 403]);
		}

		if (!empty($settings['webhookUsername']) && !empty($settings['webhookPassword'])) {
			$auth = (string) $request->get_header('authorization');
			if (0 !== strpos($auth, 'Basic ')) {
				return new \WP_Error('auth_required', __('Basic auth required', 'digital-apps-payments-for-adyen'), ['status' => 401]);
			}
			$decoded = base64_decode(substr($auth, 6), true);
			if (false === $decoded || false === strpos($decoded, ':')) {
				return new \WP_Error('invalid_credentials', __('Invalid webhook credentials', 'digital-apps-payments-for-adyen'), ['status' => 403]);
			}
			list($user, $pass) = explode(':', $decoded, 2);
			if (!hash_equals((string) $settings['webhookUsername'], $user) || !hash_equals((string) $settings['webhookPassword'], $pass)) {
				return new \WP_Error('invalid_credentials', __('Invalid webhook credentials', 'digital-apps-payments-for-adyen'), ['status' => 403]);
			}
		}

		$data = json_decode($request->get_body(), true);
		if (!is_array($data) || !array_key_exists('live', $data) || !isset($data['notificationItems']) || !is_array($data['notificationItems'])) {
			return new \WP_Error('invalid_payload', __('Invalid webhook payload', 'digital-apps-payments-for-adyen'), ['status' => 400]);
		}
		$live = true === $data['live'] || 'true' === strtolower((string) $data['live']);
		$profile = Settings_Store::get_environment(!$live, $settings);
		$hmac_keys = array_values(array_filter([
			(string) ($profile['hmacKey'] ?? ''),
			(string) ($profile['previousHmacKey'] ?? ''),
		]));
		if (empty($hmac_keys)) {
			return new \WP_Error('hmac_not_configured', __('Webhook HMAC validation is not configured for this environment', 'digital-apps-payments-for-adyen'), ['status' => 503]);
		}
		if (empty($data['notificationItems'])) {
			return new \WP_Error('invalid_payload', __('Invalid webhook payload', 'digital-apps-payments-for-adyen'), ['status' => 400]);
		}

		$validator = new HmacSignature();
		foreach ($data['notificationItems'] as $item) {
			$notification = $item['NotificationRequestItem'] ?? null;
			$signature = is_array($notification) ? ($notification['additionalData']['hmacSignature'] ?? '') : '';
			if (!is_array($notification) || '' === $signature) {
				return new \WP_Error('hmac_missing', __('HMAC signature missing', 'digital-apps-payments-for-adyen'), ['status' => 403]);
			}
			$valid = false;
			foreach ($hmac_keys as $hmac_key) {
				if ($validator->isValidNotificationHMAC($hmac_key, $notification)) {
					$valid = true;
					break;
				}
			}
			if (!$valid) {
				return new \WP_Error('hmac_invalid', __('Invalid HMAC signature', 'digital-apps-payments-for-adyen'), ['status' => 403]);
			}
		}

		return true;
	}

	/** Persist verified items and acknowledge before business processing. */
	public function handle_notification($request)
	{
		$data = json_decode($request->get_body(), true);
		$live = true === $data['live'] || 'true' === strtolower((string) $data['live']);
		$inbox = new Adyen_Webhook_Inbox();
		$accepted = 0;
		foreach ($data['notificationItems'] as $item) {
			$notification = $item['NotificationRequestItem'] ?? null;
			if (!is_array($notification) || false === $inbox->ingest($notification, $live)) {
				return new WP_REST_Response(['accepted' => false], 500);
			}
			$accepted++;
		}

		return new WP_REST_Response(['accepted' => true, 'notificationCount' => $accepted], 202);
	}

	/** Apply one asynchronously claimed inbox record. Throws to activate retry. */
	public function process_inbox_record($row)
	{
		$item = json_decode((string) $row->payload, true);
		if (!is_array($item)) {
			throw new \RuntimeException('Webhook inbox payload is invalid.');
		}
		$order = $this->find_order($item);
		if (!$order) {
			throw new \RuntimeException('No WooCommerce order matches the webhook references.');
		}

		$profile = Settings_Store::get_environment('test' === (string) $row->environment);
		$expected_merchant = (string) ($profile['merchantAccount'] ?? '');
		$event_merchant = (string) ($item['merchantAccountCode'] ?? '');
		if ('' === $expected_merchant || '' === $event_merchant || !hash_equals($expected_merchant, $event_merchant)) {
			throw new \RuntimeException('Webhook merchant account does not match the configured environment.');
		}

		$stored_merchant_reference = (string) $order->get_meta('_digitalapps_payments_for_adyen_merchant_reference', true);
		$event_merchant_reference = (string) ($item['merchantReference'] ?? '');
		$original_reference = (string) ($item['originalReference'] ?? '');
		if ('' !== $original_reference) {
			$stored_psp_reference = (string) $order->get_transaction_id();
			if ('' === $stored_psp_reference || !hash_equals($stored_psp_reference, $original_reference)) {
				throw new \RuntimeException('Webhook original reference does not match the order payment.');
			}
		} elseif (
			'' !== $stored_merchant_reference
			&& '' !== $event_merchant_reference
			&& !hash_equals($stored_merchant_reference, $event_merchant_reference)
		) {
			throw new \RuntimeException('Webhook merchant reference does not match the order.');
		}

		$order_environment = (string) $order->get_meta('_digitalapps_payments_for_adyen_environment', true);
		if ('' !== $order_environment && $order_environment !== (string) $row->environment) {
			throw new \RuntimeException('Webhook environment does not match the order environment.');
		}

		$family = $this->event_family($item);
		$event_timestamp = strtotime((string) ($item['eventDate'] ?? '')) ?: strtotime($row->event_date . ' UTC');
		$dates = $order->get_meta(self::EVENT_DATES_META_KEY, true);
		$dates = is_array($dates) ? $dates : [];
		if (isset($dates[$family]) && (int) $dates[$family] > $event_timestamp) {
			(new Logger())->log('INFO', [
				'message' => 'Stale Adyen webhook event skipped',
				'order_id' => $order->get_id(),
				'event_code' => $item['eventCode'] ?? '',
			]);
			return;
		}

		$capture_mode = (string) $order->get_meta('_digitalapps_payments_for_adyen_capture_mode', true);
		if (!in_array($capture_mode, ['automatic', 'manual'], true)) {
			$capture_mode = 'manual' === (Settings_Store::get()['captureMode'] ?? '') ? 'manual' : 'automatic';
		}
		$this->process_event($order, $item, $capture_mode);
		$this->reconcile_operation($order, $item);
		$dates[$family] = $event_timestamp;
		$order->update_meta_data(self::EVENT_DATES_META_KEY, $dates);
		$order->save();
		do_action('digitalapps_payments_for_adyen_webhook_event_processed', $item['eventCode'] ?? '', $item, $order);
	}

	/** Correlate by original PSP, event PSP, merchant reference, and stored reference. */
	private function find_order(array $item)
	{
		$references = array_unique(array_filter([
			(string) ($item['originalReference'] ?? ''),
			(string) ($item['pspReference'] ?? ''),
		]));
		foreach ($references as $reference) {
			$orders = wc_get_orders(['transaction_id' => $reference, 'limit' => 2, 'return' => 'objects']);
			$orders = array_values(array_filter($orders, static function ($order) {
				return 0 === strpos((string) $order->get_payment_method(), 'digitalapps_payments_for_adyen_');
			}));
			if (1 === count($orders)) {
				return $orders[0];
			}
		}

		$merchant_reference = (string) ($item['merchantReference'] ?? '');
		if ('' !== $merchant_reference) {
			$orders = wc_get_orders([
				'limit' => 2,
				'return' => 'objects',
				'meta_key' => '_digitalapps_payments_for_adyen_merchant_reference',
				'meta_value' => $merchant_reference,
			]);
			$orders = array_values(array_filter($orders, static function ($order) {
				return 0 === strpos((string) $order->get_payment_method(), 'digitalapps_payments_for_adyen_');
			}));
			if (1 === count($orders)) {
				return $orders[0];
			}
		}

		return null;
	}

	/** Apply supported Standard events with monotonic terminal-state guards. */
	private function process_event($order, array $item, $capture_mode)
	{
		$event_code = (string) ($item['eventCode'] ?? '');
		$psp_reference = sanitize_text_field($item['pspReference'] ?? '');
		$original_reference = sanitize_text_field($item['originalReference'] ?? '');
		$success = true === ($item['success'] ?? false) || 'true' === strtolower((string) ($item['success'] ?? ''));
		$terminal = $order->has_status(['cancelled', 'refunded']);
		if ('AUTHORISATION' === $event_code && '' !== $psp_reference && '' === (string) $order->get_transaction_id()) {
			$order->set_transaction_id($psp_reference);
		}

		switch ($event_code) {
			case 'AUTHORISATION':
				if ($success && !$terminal && !$order->is_paid()) {
					if ('manual' === $capture_mode) {
						$order->update_status('on-hold', __('Adyen payment authorized, awaiting manual capture.', 'digital-apps-payments-for-adyen'));
					} else {
						$order->payment_complete($psp_reference);
					}
				} elseif (!$success && !$terminal && !$order->is_paid()) {
					$order->update_status('failed', __('Adyen webhook confirmed that the card authorization was unsuccessful.', 'digital-apps-payments-for-adyen'));
				}
				break;

			case 'CAPTURE':
				if ($success && !$terminal && 'manual' === $capture_mode && !$order->is_paid()) {
					$order->payment_complete($original_reference ?: $order->get_transaction_id());
				}
				$order->add_order_note($success
					? __('Adyen webhook confirmed the capture.', 'digital-apps-payments-for-adyen')
					: __('Adyen webhook reported an unsuccessful capture request.', 'digital-apps-payments-for-adyen'));
				break;

			case 'CAPTURE_FAILED':
				if (!$terminal && !$order->has_status('on-hold')) {
					$order->update_status('on-hold', __('Adyen reported that capture failed. Review the order and retry only when safe.', 'digital-apps-payments-for-adyen'));
				}
				$order->add_order_note(__('Adyen webhook confirmed capture failure. Review the protected logs and Adyen Customer Area.', 'digital-apps-payments-for-adyen'));
				break;

			case 'REFUND':
				if ($success) {
					$full_refund = $this->record_confirmed_refund($order, $item);
					if ($full_refund && !$order->has_status('refunded')) {
						$order->update_status('refunded', __('Adyen webhook confirmed cumulative refunds for the full order total.', 'digital-apps-payments-for-adyen'));
					} else {
						$order->add_order_note(__('Adyen webhook confirmed a partial refund.', 'digital-apps-payments-for-adyen'));
					}
				} else {
					$order->add_order_note(__('Adyen webhook reported an unsuccessful refund.', 'digital-apps-payments-for-adyen'));
				}
				break;

			case 'REFUND_FAILED':
				$order->add_order_note(__('Adyen webhook confirmed refund failure. The WooCommerce refund record requires merchant reconciliation.', 'digital-apps-payments-for-adyen'));
				break;

			case 'REFUNDED_REVERSED':
				$this->remove_reversed_confirmed_refund($order, $item);
				$order->update_status('on-hold', __('Adyen reversed a refund. Review the payment and order ledger.', 'digital-apps-payments-for-adyen'));
				break;

			case 'CANCELLATION':
				if ($success && !$order->has_status('refunded')) {
					$order->update_status('cancelled', __('Adyen webhook confirmed cancellation of the authorization.', 'digital-apps-payments-for-adyen'));
				} elseif (!$success) {
					$order->add_order_note(__('Adyen webhook reported an unsuccessful cancellation.', 'digital-apps-payments-for-adyen'));
				}
				break;

			case 'CANCEL_OR_REFUND':
				if ($success) {
					$order->update_status($order->is_paid() ? 'refunded' : 'cancelled', __('Adyen webhook confirmed the reversal.', 'digital-apps-payments-for-adyen'));
				} else {
					$order->add_order_note(__('Adyen webhook reported an unsuccessful reversal.', 'digital-apps-payments-for-adyen'));
				}
				break;

			case 'CHARGEBACK':
				if ($success) {
					$order->update_status('on-hold', __('Adyen reported a chargeback. Merchant review is required.', 'digital-apps-payments-for-adyen'));
				}
				break;

			default:
				/* translators: %s: Adyen event code. */
				$order->add_order_note(sprintf(__('Adyen webhook event recorded without an automatic order transition: %s', 'digital-apps-payments-for-adyen'), $event_code));
				break;
		}

		$order->save();
	}

	/** Track each successful refund PSP reference and use the cumulative amount. */
	private function record_confirmed_refund($order, array $item)
	{
		$refunds = $order->get_meta(self::CONFIRMED_REFUNDS_META_KEY, true);
		$refunds = is_array($refunds) ? $refunds : [];
		$psp_reference = (string) ($item['pspReference'] ?? '');
		$amount = $item['amount'] ?? [];
		$currency = strtoupper((string) ($amount['currency'] ?? ''));
		$value = is_numeric($amount['value'] ?? null) ? (int) $amount['value'] : 0;
		if ('' !== $psp_reference && $value > 0 && $currency === strtoupper($order->get_currency())) {
			$refunds[$psp_reference] = $value;
			$order->update_meta_data(self::CONFIRMED_REFUNDS_META_KEY, $refunds);
		}
		$total = array_sum(array_map('intval', $refunds));
		return $total >= Adyen_Amount::to_minor_units($order->get_total(), $order->get_currency());
	}

	/** Update the matching locally initiated modification. */
	private function reconcile_operation($order, array $item)
	{
		$event_code = (string) ($item['eventCode'] ?? '');
		$type_map = [
			'CAPTURE' => 'capture',
			'CAPTURE_FAILED' => 'capture',
			'REFUND' => 'refund',
			'REFUND_FAILED' => 'refund',
			'REFUNDED_REVERSED' => 'refund',
			'CANCELLATION' => 'cancel',
			'CANCEL_OR_REFUND' => 'reverse',
		];
		if (!isset($type_map[$event_code])) {
			return;
		}
		$operations = $order->get_meta(Adyen_Operation::META_KEY, true);
		if (!is_array($operations)) {
			return;
		}
		$references = [(string) ($item['pspReference'] ?? ''), (string) ($item['originalReference'] ?? '')];
		$matched_id = '';
		foreach ($operations as $operation_id => $operation) {
			if (!is_array($operation) || $type_map[$event_code] !== ($operation['type'] ?? '')) {
				continue;
			}
			if (in_array((string) ($operation['pspReference'] ?? ''), $references, true)) {
				$matched_id = $operation_id;
				break;
			}
			if ('' === $matched_id && 'initiated' === ($operation['status'] ?? '')) {
				$matched_id = $operation_id;
			}
		}
		if ('' === $matched_id && 'REFUNDED_REVERSED' === $event_code) {
			$matched_id = $this->find_reversed_refund_operation_id($order, $item);
		}
		if ('' !== $matched_id) {
			$failed = in_array($event_code, ['CAPTURE_FAILED', 'REFUND_FAILED', 'REFUNDED_REVERSED'], true)
				|| !(true === ($item['success'] ?? false) || 'true' === strtolower((string) ($item['success'] ?? '')));
			if ($failed && 'refund' === $type_map[$event_code]) {
				$this->reconcile_failed_woocommerce_refund($order, $operations[$matched_id], 'REFUNDED_REVERSED' === $event_code);
			}
			Adyen_Operation::mark($order, $matched_id, 'REFUNDED_REVERSED' === $event_code ? 'reversed' : ($failed ? 'failed' : 'succeeded'), [
				'webhookEventCode' => $event_code,
				'webhookPspReference' => sanitize_text_field($item['pspReference'] ?? ''),
				'webhookEventDate' => sanitize_text_field($item['eventDate'] ?? ''),
			]);
		}
	}

	/** Preserve a failed or reversed refund record while removing it from refunded totals. */
	private function reconcile_failed_woocommerce_refund($order, array $operation, $reversed = false)
	{
		$refund_id = (int) ($operation['wooRefundId'] ?? 0);
		$refund = $refund_id ? wc_get_order($refund_id) : false;
		if (!$refund || !is_a($refund, 'WC_Order_Refund')) {
			return;
		}
		if ((float) $refund->get_amount() > 0) {
			$original_amount = $refund->get_amount();
			$refund->update_meta_data('_digitalapps_payments_for_adyen_failed_amount', $original_amount);
			$refund->set_amount(0);
			$refund->set_reason(trim($refund->get_reason() . ' ' . ($reversed
				? __('Adyen reported that this refund was reversed.', 'digital-apps-payments-for-adyen')
				: __('Adyen reported that this refund failed.', 'digital-apps-payments-for-adyen'))));
			$refund->save();
			$order->add_order_note($reversed
				? __('The reversed Adyen refund was retained as a zero-value WooCommerce audit record, restoring the refundable balance.', 'digital-apps-payments-for-adyen')
				: __('The failed Adyen refund was retained as a zero-value WooCommerce audit record, so the amount can be retried safely.', 'digital-apps-payments-for-adyen'));
		}
		$pre_refund_status = sanitize_key((string) ($operation['preRefundStatus'] ?? ''));
		if ($order->has_status('refunded') && in_array($pre_refund_status, ['processing', 'completed', 'on-hold'], true)) {
			$order->update_status($pre_refund_status, __('The Adyen refund failed, so the prior paid order status was restored.', 'digital-apps-payments-for-adyen'));
		}
	}

	/** Remove a reversed refund from cumulative confirmed totals when its local operation is unambiguous. */
	private function remove_reversed_confirmed_refund($order, array $item)
	{
		$operation_id = $this->find_reversed_refund_operation_id($order, $item);
		$operation = '' !== $operation_id ? Adyen_Operation::get($order, $operation_id) : [];
		$confirmed_psp = (string) ($operation['webhookPspReference'] ?? '');
		$refunds = $order->get_meta(self::CONFIRMED_REFUNDS_META_KEY, true);
		if ('' === $confirmed_psp || !is_array($refunds) || !array_key_exists($confirmed_psp, $refunds)) {
			$order->add_order_note(__('Adyen reversed a refund, but no unique local confirmed-refund operation could be matched. Manual reconciliation is required.', 'digital-apps-payments-for-adyen'));
			return;
		}
		unset($refunds[$confirmed_psp]);
		$order->update_meta_data(self::CONFIRMED_REFUNDS_META_KEY, $refunds);
	}

	/** Match a reversal to one successful refund by payment, amount, currency, and monotonic event time. */
	private function find_reversed_refund_operation_id($order, array $item)
	{
		$operations = $order->get_meta(Adyen_Operation::META_KEY, true);
		if (!is_array($operations)) {
			return '';
		}
		$value = is_numeric($item['amount']['value'] ?? null) ? (int) $item['amount']['value'] : -1;
		$currency = strtoupper((string) ($item['amount']['currency'] ?? ''));
		$original_reference = (string) ($item['originalReference'] ?? '');
		$event_timestamp = strtotime((string) ($item['eventDate'] ?? '')) ?: 0;
		foreach (array_reverse($operations, true) as $operation_id => $operation) {
			if (
				!is_array($operation)
				|| 'refund' !== ($operation['type'] ?? '')
				|| 'succeeded' !== ($operation['status'] ?? '')
				|| $value !== (int) ($operation['amountValue'] ?? -2)
				|| $currency !== strtoupper((string) ($operation['currency'] ?? ''))
				|| $original_reference !== (string) ($operation['originalReference'] ?? '')
			) {
				continue;
			}
			$confirmed_timestamp = strtotime((string) ($operation['webhookEventDate'] ?? '')) ?: 0;
			if (0 === $event_timestamp || 0 === $confirmed_timestamp || $event_timestamp >= $confirmed_timestamp) {
				return (string) $operation_id;
			}
		}
		return '';
	}

	private function event_family(array $item)
	{
		$event_code = (string) ($item['eventCode'] ?? 'UNKNOWN');
		if (in_array($event_code, ['CAPTURE', 'CAPTURE_FAILED'], true)) {
			return 'capture:' . (string) ($item['originalReference'] ?? 'payment');
		}
		if (in_array($event_code, ['REFUND', 'REFUND_FAILED', 'REFUNDED_REVERSED'], true)) {
			return 'refund:' . (string) ($item['pspReference'] ?? $item['originalReference'] ?? 'unknown');
		}
		if (in_array($event_code, ['CANCELLATION', 'CANCEL_OR_REFUND'], true)) {
			return 'reversal:' . (string) ($item['originalReference'] ?? 'payment');
		}
		return strtolower($event_code);
	}
}
