<?php

namespace DigitalApps\PaymentsForAdyen;

defined('ABSPATH') || exit;

/** Durable ingress, deduplication, retry, and dead-letter handling for webhooks. */
final class Adyen_Webhook_Inbox
{
	const TABLE_SUFFIX = 'digitalapps_payments_for_adyen_webhook_inbox';
	const PROCESS_HOOK = 'digitalapps_payments_for_adyen_process_webhook_inbox';
	const MAX_ATTEMPTS = 5;

	/** Create or upgrade the inbox schema idempotently. */
	public static function create_table()
	{
		global $wpdb;
		$table = $wpdb->prefix . self::TABLE_SUFFIX;
		$charset_collate = $wpdb->get_charset_collate();
		$sql = "CREATE TABLE {$table} (
			id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
			event_key char(64) NOT NULL,
			event_code varchar(64) NOT NULL,
			psp_reference varchar(64) NOT NULL,
			original_reference varchar(64) NOT NULL,
			merchant_reference varchar(160) NOT NULL,
			event_date datetime NOT NULL,
			environment varchar(4) NOT NULL,
			payload longtext NOT NULL,
			status varchar(16) NOT NULL DEFAULT 'pending',
			attempts tinyint unsigned NOT NULL DEFAULT 0,
			available_at datetime NOT NULL,
			last_error text NOT NULL,
			created_at datetime NOT NULL,
			updated_at datetime NOT NULL,
			PRIMARY KEY  (id),
			UNIQUE KEY event_key (event_key),
			KEY status_available (status, available_at),
			KEY original_reference (original_reference),
			KEY merchant_reference (merchant_reference)
		) {$charset_collate};";

		require_once ABSPATH . 'wp-admin/includes/upgrade.php';
		dbDelta($sql);
	}

	/**
	 * Insert or refresh one signed event using Adyen's eventCode + pspReference identity.
	 *
	 * @param array $item Verified NotificationRequestItem.
	 * @param bool  $live Whether the payload is live.
	 * @return int|false Row ID, including an existing duplicate row.
	 */
	public function ingest(array $item, $live)
	{
		$event_code = sanitize_text_field($item['eventCode'] ?? '');
		$psp_reference = sanitize_text_field($item['pspReference'] ?? '');
		if ('' === $event_code || '' === $psp_reference) {
			return false;
		}

		$event_key = hash('sha256', $event_code . '|' . $psp_reference);
		$event_timestamp = strtotime((string) ($item['eventDate'] ?? ''));
		$event_date = gmdate('Y-m-d H:i:s', false === $event_timestamp ? time() : $event_timestamp);
		$payload = $item;
		unset($payload['additionalData']['hmacSignature']);
		$now = gmdate('Y-m-d H:i:s');

		global $wpdb;
		$table = $wpdb->prefix . self::TABLE_SUFFIX;
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Durable webhook inbox write.
		$inserted = $wpdb->query($wpdb->prepare(
			"INSERT IGNORE INTO {$table}
			(event_key, event_code, psp_reference, original_reference, merchant_reference, event_date, environment, payload, status, attempts, available_at, last_error, created_at, updated_at)
			VALUES (%s, %s, %s, %s, %s, %s, %s, %s, 'pending', 0, %s, '', %s, %s)",
			$event_key,
			$event_code,
			$psp_reference,
			sanitize_text_field($item['originalReference'] ?? ''),
			sanitize_text_field($item['merchantReference'] ?? ''),
			$event_date,
			$live ? 'live' : 'test',
			wp_json_encode($payload),
			$now,
			$now,
			$now
		));

		if (1 === (int) $inserted) {
			$row_id = (int) $wpdb->insert_id;
			$this->enqueue($row_id);
			return $row_id;
		}

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Duplicate reconciliation lookup.
		$existing = $wpdb->get_row($wpdb->prepare("SELECT id, event_date FROM {$table} WHERE event_key = %s", $event_key));
		if (!$existing) {
			return false;
		}
		if ($event_date > (string) $existing->event_date) {
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Adyen requires the latest duplicate contents to win.
			$wpdb->update(
				$table,
				[
					'original_reference' => sanitize_text_field($item['originalReference'] ?? ''),
					'merchant_reference' => sanitize_text_field($item['merchantReference'] ?? ''),
					'event_date' => $event_date,
					'environment' => $live ? 'live' : 'test',
					'payload' => wp_json_encode($payload),
					'status' => 'pending',
					'attempts' => 0,
					'available_at' => $now,
					'last_error' => '',
					'updated_at' => $now,
				],
				['id' => (int) $existing->id],
				['%s', '%s', '%s', '%s', '%s', '%s', '%d', '%s', '%s', '%s'],
				['%d']
			);
			$this->enqueue((int) $existing->id);
		}

		return (int) $existing->id;
	}

	/** Claim and process one inbox row with bounded retries. */
	public function process($row_id)
	{
		global $wpdb;
		$table = $wpdb->prefix . self::TABLE_SUFFIX;
		$now = gmdate('Y-m-d H:i:s');
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Atomic worker claim.
		$claimed = $wpdb->query($wpdb->prepare(
			"UPDATE {$table} SET status = 'processing', updated_at = %s WHERE id = %d AND status IN ('pending', 'failed') AND available_at <= %s",
			$now,
			(int) $row_id,
			$now
		));
		if (1 !== (int) $claimed) {
			return;
		}

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Claimed custom-table row.
		$row = $wpdb->get_row($wpdb->prepare("SELECT * FROM {$table} WHERE id = %d", (int) $row_id));
		if (!$row) {
			return;
		}

		try {
			$controller = new \DigitalApps\PaymentsForAdyen\Api\Webhook_Controller();
			$controller->process_inbox_record($row);
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Worker completion state.
			$wpdb->update($table, ['status' => 'completed', 'last_error' => '', 'updated_at' => gmdate('Y-m-d H:i:s')], ['id' => (int) $row_id], ['%s', '%s', '%s'], ['%d']);
		} catch (\Throwable $exception) {
			$attempts = ((int) $row->attempts) + 1;
			$dead = $attempts >= self::MAX_ATTEMPTS;
			$available_at = gmdate('Y-m-d H:i:s', time() + min(3600, 60 * (2 ** max(0, $attempts - 1))));
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Worker retry state.
			$wpdb->update(
				$table,
				[
					'status' => $dead ? 'dead' : 'failed',
					'attempts' => $attempts,
					'available_at' => $available_at,
					'last_error' => substr($exception->getMessage(), 0, 1000),
					'updated_at' => gmdate('Y-m-d H:i:s'),
				],
				['id' => (int) $row_id],
				['%s', '%d', '%s', '%s', '%s'],
				['%d']
			);
			(new Logger())->log('ERROR', [
				'message' => $dead ? 'Adyen webhook moved to dead-letter state' : 'Adyen webhook processing scheduled for retry',
				'inbox_id' => (int) $row_id,
				'attempts' => $attempts,
			]);
			if (!$dead) {
				$this->enqueue((int) $row_id, strtotime($available_at . ' UTC'));
			}
		}
	}

	private function enqueue($row_id, $timestamp = null)
	{
		$timestamp = $timestamp ?: time();
		if (function_exists('as_schedule_single_action')) {
			as_schedule_single_action($timestamp, self::PROCESS_HOOK, [(int) $row_id], 'digital-apps-payments-for-adyen', true);
			return;
		}
		wp_schedule_single_event($timestamp, self::PROCESS_HOOK, [(int) $row_id]);
	}
}
