<?php
/**
 * AnyCoin API.
 *
 * @package AnyCoin_Payment_Gateway
 */

defined( 'ABSPATH' ) || exit;

/**
 * AnyCoin API Handler
 *
 * Handles communication with cryptocurrency APIs for payment processing.
 */
class AnyCoin_API {

	// Constants.
	const DEFAULT_TIMEOUT             = 30;
	const CONNECTION_TEST_TIMEOUT     = 15;
	const PRICE_API_TIMEOUT           = 10;
	const MEXC_PRICE_API_URL          = 'https://api.mexc.com/api/v3/ticker/price?symbol=ZANOUSDT';
	const JSONRPC_VERSION             = '2.0';
	const DEFAULT_BLOCKS_LIMIT        = 5;
	const AMOUNT_TOLERANCE_PERCENTAGE = 0.02; // 2% tolerance for amount matching.
	const DEFAULT_BUFFER_PERCENTAGE   = 1; // 1% price buffer.

	// Asset configurations.
	const ASSET_CONFIGS = [
		'd6329b5b1f7c0805b5c345f4957554002a2f557845f64d7645dae0e051a6498a' => [
			'symbol'   => 'ZANO',
			'decimals' => 12,
			'divisor'  => 1000000000000, // 10^12.
		],
		'86143388bd056a8f0bab669f78f14873fac8e2dd8d57898cdb725a2d5e2e4f8f' => [
			'symbol'   => 'FUSD',
			'decimals' => 4,
			'divisor'  => 10000, // 10^4.
		],
	];

	const DEFAULT_ZANO_ASSET_ID = 'd6329b5b1f7c0805b5c345f4957554002a2f557845f64d7645dae0e051a6498a';

	/**
	 * API URL.
	 *
	 * @var string
	 */
	private $api_url;
	/**
	 * Wallet address.
	 *
	 * @var string
	 */
	private $wallet_address;
	/**
	 * Private view key.
	 *
	 * @var string
	 */
	private $view_key;
	/**
	 * Payment ID verification API base URL.
	 *
	 * @var string
	 */
	private $payment_id_api_url;
	/**
	 * Debug mode enabled.
	 *
	 * @var bool
	 */
	private $debug;

	/**
	 * Constructor.
	 *
	 * @param array $config Configuration settings.
	 */
	public function __construct( $config = [] ) {
		$this->api_url            = $config['api_url'] ?? '';
		$this->wallet_address     = $config['wallet_address'] ?? '';
		$this->view_key           = $config['view_key'] ?? '';
		$this->payment_id_api_url = $config['payment_id_api_url'] ?? AnyCoin_Constants::PAYMENT_VERIFICATION_API;
		$this->debug              = $config['debug'] ?? false;
	}

	/**
	 * Check payment status.
	 *
	 * @param int $order_id Order ID.
	 * @return array Payment status.
	 */
	public function check_payment_status( $order_id ) {
		global $wpdb;

		$this->log( 'Checking payment status for order #' . $order_id );

		$payment = $this->get_payment_record( $order_id );
		if ( ! $payment ) {
			$this->log( 'No payment record found for order #' . $order_id );
			return [ 'status' => 'not_found' ];
		}

		$this->log( 'Found payment record for order #' . $order_id );

		if ( $payment['status'] === 'confirmed' ) {
			return $this->build_confirmed_status_response( $payment, $order_id );
		}

		$requirements = $this->get_payment_requirements( $payment );
		$this->log(
			sprintf(
				'Required amount: %f %s, Required confirmations: %d',
				$requirements['amount'],
				$requirements['asset_symbol'],
				$requirements['confirmations']
			)
		);

		$transactions = $this->get_recent_transactions();
		if ( is_wp_error( $transactions ) ) {
			$this->log( 'Error checking transactions: ' . $transactions->get_error_message() );
			return [
				'status'  => 'error',
				'message' => $transactions->get_error_message(),
			];
		}

		if ( empty( $transactions ) ) {
			$this->log( 'No transactions found for order #' . $order_id );
			return [
				'status'  => 'pending',
				'message' => 'No transactions found',
			];
		}

		$this->log( 'Found ' . count( $transactions ) . ' transactions' );

		return $this->process_transactions_for_payment( $transactions, $payment, $requirements, $order_id );
	}

	/**
	 * Generate a unique payment ID.
	 *
	 * 8 bytes (16 hex chars) - the maximum size supported by integrated
	 * addresses and intrinsic payment IDs since Zano HF6.
	 *
	 * @return string Unique payment ID.
	 */
	public function generate_payment_id() {

		return bin2hex( random_bytes( 8 ) );
	}

	/**
	 * Generate an integrated address from a regular address and payment ID.
	 *
	 * @param string $payment_id Payment ID to integrate.
	 * @return string|WP_Error Integrated address or error.
	 */
	public function generate_integrated_address( $payment_id ) {
		if ( empty( $this->wallet_address ) ) {
			return new WP_Error( 'missing_address', 'Wallet address not set' );
		}

		$this->log( 'Generating integrated address for new payment' );

		try {
			$params = [
				'regular_address' => $this->wallet_address,
				'payment_id'      => $payment_id,
			];

			$response = $this->api_request( 'get_integrated_address', $params );

			if ( is_wp_error( $response ) ) {
				$this->log( 'Error generating integrated address: ' . $response->get_error_message() );
				return $response;
			}

			if ( isset( $response['integrated_address'] ) ) {
				$this->log( 'Successfully generated integrated address' );
				return $response['integrated_address'];
			} else {
				$this->log( 'Invalid response from integrated address API' );
				return new WP_Error( 'invalid_response', 'Invalid response from integrated address API' );
			}
		} catch ( Exception $e ) {
			$this->log( 'Exception generating integrated address: ' . $e->getMessage() );
			return new WP_Error( 'api_error', $e->getMessage() );
		}
	}

	/**
	 * Verify payment ID from transaction using external API.
	 *
	 * @param string $tx_hash Transaction hash.
	 * @return array|WP_Error Payment verification result.
	 */
	public function verify_payment_id( $tx_hash ) {
		if ( empty( $this->wallet_address ) || empty( $this->view_key ) ) {
			return new WP_Error( 'missing_credentials', 'Wallet address or view key not set' );
		}

		$this->log( 'Verifying payment for transaction: ' . $tx_hash );

		try {
			// Build API URL for payment ID verification.
			$base_url = rtrim( $this->payment_id_api_url, '/' );
			$api_url  = $base_url . '/api/decode-transaction/' . $tx_hash;
			$api_url  = add_query_arg(
				[
					'walletAddress'  => $this->wallet_address,
					'privateViewKey' => $this->view_key,
				],
				$api_url
			);

			$this->log( 'Calling payment verification API for transaction: ' . $tx_hash );

			// Make API request.
			$response = wp_remote_get(
				$api_url,
				[
					'timeout' => self::CONNECTION_TEST_TIMEOUT,
					'headers' => [
						'Content-Type' => 'application/json',
					],
				]
			);

			if ( is_wp_error( $response ) ) {
				$this->log( 'Payment ID verification API error: ' . $response->get_error_message() );
				return $response;
			}

			$body = wp_remote_retrieve_body( $response );
			$data = json_decode( $body, true, 512, JSON_BIGINT_AS_STRING );

			if ( json_last_error() !== JSON_ERROR_NONE ) {
				$this->log( 'Invalid JSON response from payment ID verification API' );
				return new WP_Error( 'invalid_json', 'Invalid JSON response from payment ID verification API' );
			}

			if ( ! isset( $data['success'] ) || ! $data['success'] ) {
				$error_message = isset( $data['error'] ) ? $data['error'] : 'Unknown error';
				$this->log( 'Payment ID verification failed: ' . $error_message );
				return new WP_Error( 'verification_failed', $error_message );
			}

			$this->log( 'Payment verification successful for transaction: ' . $tx_hash );
			return $data;

		} catch ( Exception $e ) {
			$this->log( 'Exception verifying payment ID: ' . $e->getMessage() );
			return new WP_Error( 'api_error', $e->getMessage() );
		}
	}

	/**
	 * Get recent transactions from the Zano blockchain.
	 *
	 * @return array|WP_Error Recent transactions or error.
	 */
	public function get_recent_transactions() {
		if ( empty( $this->wallet_address ) || empty( $this->view_key ) ) {
			return new WP_Error( 'missing_credentials', 'Wallet address or view key not set' );
		}

		$view_key = trim( $this->view_key );

		$this->log( 'Getting recent transactions with find_outs_in_recent_blocks method.' );
		$this->log( 'Performing transaction lookup.' );

		try {
			if ( ! function_exists( 'curl_init' ) ) {
				return new WP_Error( 'curl_not_available', 'cURL is not available on this server' );
			}

			$params = array(
				'address'      => $this->wallet_address,
				'viewkey'      => $view_key,
				'blocks_limit' => self::DEFAULT_BLOCKS_LIMIT,
			);

			$post_data = wp_json_encode(
				[
					'jsonrpc' => self::JSONRPC_VERSION,
					'id'      => '1',
					'method'  => 'find_outs_in_recent_blocks',
					'params'  => $params,
				]
			);

			$this->log( 'API request payload: ' . $post_data );

			$args = [
				'body'      => $post_data,
				'headers'   => [
					'Content-Type' => 'application/json',
				],
				'timeout'   => self::DEFAULT_TIMEOUT,
				'sslverify' => false,
			];

			$wp_response = wp_remote_post( $this->api_url, $args );
			$http_code   = wp_remote_retrieve_response_code( $wp_response );

			if ( is_wp_error( $wp_response ) ) {
				$curl_error = $wp_response->get_error_message();
				$response   = false;
			} else {
				$response   = wp_remote_retrieve_body( $wp_response );
				$curl_error = '';
			}

			if ( $response === false ) {
				$this->log( 'Transaction fetch failed: ' . $curl_error );
				return new WP_Error( 'api_error', sprintf( 'API request failed: %s', $curl_error ) );
			}

			if ( $http_code !== 200 ) {
				$this->log( 'Transaction fetch failed with HTTP code ' . $http_code );
				return new WP_Error( 'api_error', sprintf( 'API request failed with code %s', $http_code ) );
			}

			// JSON_BIGINT_AS_STRING keeps 64-bit intrinsic payment IDs exact (HF6);
			// values >= 2^63 would otherwise be truncated to floats by json_decode.
			$data = json_decode( $response, true, 512, JSON_BIGINT_AS_STRING );

			if ( null === $data ) {
				$this->log( 'Transaction response JSON decoding failed' );
				return new WP_Error( 'api_error', 'Invalid API response format' );
			}

			if ( isset( $data['error'] ) ) {
				$error_msg = isset( $data['error']['message'] ) ? $data['error']['message'] : wp_json_encode( $data['error'] );
				$this->log( 'API returned error: ' . $error_msg );
				return new WP_Error( 'api_error', $error_msg );
			}

			return $this->process_transaction_outputs( $data['result'] ?? [] );

		} catch ( Exception $e ) {
			$this->log( 'Exception in get_recent_transactions: ' . $e->getMessage() );
			return new WP_Error( 'api_error', $e->getMessage() );
		}
	}

	/**
	 * Process transaction outputs into a standardized format.
	 *
	 * Since Zano HF6, each output can carry its own intrinsic payment ID and a
	 * single transaction may contain multiple payments with different payment
	 * IDs and assets. Outputs are therefore grouped by (tx, asset, payment ID)
	 * so each group represents one logical payment.
	 *
	 * @param array $result API result data.
	 * @return array Formatted transactions.
	 */
	private function process_transaction_outputs( $result ) {
		$aggregated = [];

		$this->log( 'Processing API result for transaction lookup.' );

		if ( empty( $result ) || ! is_array( $result ) ) {
			$this->log( 'Empty or invalid result data.' );
			return [];
		}

		$blockchain_height = isset( $result['blockchain_top_block_height'] ) ?
			intval( $result['blockchain_top_block_height'] ) : 0;

		$this->log( 'Current blockchain height: ' . $blockchain_height );

		$asset_info = self::ASSET_CONFIGS;

		if ( ! empty( $result['outputs'] ) && is_array( $result['outputs'] ) ) {
			foreach ( $result['outputs'] as $output ) {
				$this->log( 'Processing transaction output.' );

				if ( ! isset( $output['tx_id'] ) ) {
					$this->log( 'Skipping output without tx_id.' );
					continue;
				}

				$tx_id = $output['tx_id'];

				$asset_id = isset( $output['asset_id'] ) ? $output['asset_id'] :
							self::DEFAULT_ZANO_ASSET_ID;

				$asset = isset( $asset_info[ $asset_id ] ) ? $asset_info[ $asset_id ] : $asset_info[ self::DEFAULT_ZANO_ASSET_ID ];

				$amount_raw = isset( $output['amount'] ) ? $output['amount'] :
								( isset( $output['value'] ) ? $output['value'] : 0 );

				$amount = (float) ( $amount_raw / $asset['divisor'] );

				$intrinsic_pid = self::normalize_intrinsic_payment_id( $output['payment_id'] ?? '' );

				$this->log(
					sprintf(
						'Converted amount: %f %s (from %s atomic units) - Asset ID: %s, Intrinsic PID: %s',
						$amount,
						$asset['symbol'],
						$amount_raw,
						$asset_id,
						( '' !== $intrinsic_pid ) ? $intrinsic_pid : '(none)'
					)
				);

				$block_height = isset( $output['tx_block_height'] ) ? intval( $output['tx_block_height'] ) : -1;

				$confirmations = ( $block_height > 0 && $blockchain_height > 0 ) ?
					( $blockchain_height - $block_height ) : 0;

				$confirmations = max( 0, $confirmations );

				$group_key = $tx_id . '|' . $asset_id . '|' . $intrinsic_pid;

				if ( isset( $aggregated[ $group_key ] ) ) {
					$aggregated[ $group_key ]['amount'] += $amount;
					$this->log(
						sprintf(
							'Aggregated additional output for tx %s (asset %s, PID %s), total amount now %f',
							$tx_id,
							$asset['symbol'],
							( '' !== $intrinsic_pid ) ? $intrinsic_pid : '(none)',
							$aggregated[ $group_key ]['amount']
						)
					);
					continue;
				}

				$this->log(
					sprintf(
						'Transaction: hash=%s, amount=%f %s, asset_id=%s, block_height=%d, confirmations=%d',
						$tx_id,
						$amount,
						$asset['symbol'],
						$asset_id,
						$block_height,
						$confirmations
					)
				);

				$aggregated[ $group_key ] = [
					'tx_hash'       => $tx_id,
					'amount'        => $amount,
					'asset_id'      => $asset_id,
					'asset_symbol'  => $asset['symbol'],
					'payment_id'    => $intrinsic_pid,
					'confirmations' => $confirmations,
					'timestamp'     => time(),
					'is_income'     => true,
					'block_height'  => $block_height,
				];
			}
		}

		$transactions = array_values( $aggregated );

		$this->log( 'Found ' . count( $transactions ) . ' recent transaction outputs' );
		return $transactions;
	}

	/**
	 * Normalize an intrinsic payment ID (HF6) to the 16-char hex format used
	 * throughout the plugin.
	 *
	 * The daemon serializes the 8-byte intrinsic PID as an unsigned 64-bit
	 * JSON number (decoded to int, float, or decimal string depending on
	 * magnitude), while wallet APIs use a hex string of the little-endian
	 * bytes. Zero / empty means "no payment ID" and returns ''.
	 *
	 * @param mixed $value Raw payment ID value from an API response.
	 * @return string 16-char lowercase hex payment ID, or '' if none.
	 */
	public static function normalize_intrinsic_payment_id( $value ) {
		if ( null === $value || '' === $value || 0 === $value || 0.0 === $value || '0' === $value ) {
			return '';
		}

		if ( is_int( $value ) ) {
			$value = (string) $value;
		} elseif ( is_float( $value ) ) {
			$value = sprintf( '%.0f', $value );
		}

		if ( ! is_string( $value ) ) {
			return '';
		}

		if ( preg_match( '/^\d+$/', $value ) ) {
			if ( '' === ltrim( $value, '0' ) ) {
				return '';
			}
			return self::decimal_string_to_le_hex( $value );
		}

		if ( preg_match( '/^[0-9a-fA-F]{16}$/', $value ) ) {
			return strtolower( $value );
		}

		return '';
	}

	/**
	 * Convert an unsigned 64-bit decimal string to 16 hex chars of its
	 * little-endian byte representation (arbitrary precision, no GMP needed).
	 *
	 * @param string $decimal Decimal digit string.
	 * @return string 16-char lowercase hex string.
	 */
	private static function decimal_string_to_le_hex( $decimal ) {
		$bytes = [];

		while ( '' !== $decimal && '0' !== $decimal && count( $bytes ) < 8 ) {
			$quotient  = '';
			$remainder = 0;
			$length    = strlen( $decimal );

			for ( $i = 0; $i < $length; $i++ ) {
				$acc       = $remainder * 10 + (int) $decimal[ $i ];
				$digit     = intdiv( $acc, 256 );
				$remainder = $acc % 256;

				if ( '' !== $quotient || $digit > 0 ) {
					$quotient .= (string) $digit;
				}
			}

			$bytes[] = $remainder;
			$decimal = ( '' === $quotient ) ? '0' : $quotient;
		}

		while ( count( $bytes ) < 8 ) {
			$bytes[] = 0;
		}

		$hex = '';
		foreach ( $bytes as $byte ) {
			$hex .= str_pad( dechex( $byte ), 2, '0', STR_PAD_LEFT );
		}

		return $hex;
	}

	/**
	 * Extract the payment entry matching an expected payment ID from a
	 * verification API response.
	 *
	 * Supports the legacy flat shape ({paymentId, amount}), a multi-payment
	 * list ({payments: [{paymentId, amount}, ...]}), and the HF6 wallet-style
	 * shape ({subtransfers_by_pid: [{payment_id, subtransfers: [...]}, ...]}),
	 * since one transaction may contain multiple payments after HF6.
	 *
	 * @param array       $verification        Decoded verification response.
	 * @param string      $expected_payment_id Expected payment ID (hex).
	 * @param string|null $asset_id            Asset ID for amount selection (optional).
	 * @return array|null ['paymentId' => ..., 'amount' => float] or null if no match.
	 */
	public static function extract_payment_for_pid( $verification, $expected_payment_id, $asset_id = null ) {
		if ( ! is_array( $verification ) ) {
			return null;
		}

		$expected = strtolower( (string) $expected_payment_id );
		if ( '' === $expected ) {
			return null;
		}

		// HF6 wallet-style: subtransfers_by_pid with per-PID subtransfer lists (atomic units).
		if ( isset( $verification['subtransfers_by_pid'] ) && is_array( $verification['subtransfers_by_pid'] ) ) {
			foreach ( $verification['subtransfers_by_pid'] as $entry ) {
				if ( ! is_array( $entry ) ) {
					continue;
				}

				$entry_pid = $entry['payment_id'] ?? '';
				if ( ! self::payment_id_matches( $entry_pid, $expected ) ) {
					continue;
				}

				$amount = 0.0;
				if ( isset( $entry['subtransfers'] ) && is_array( $entry['subtransfers'] ) ) {
					foreach ( $entry['subtransfers'] as $subtransfer ) {
						if ( ! is_array( $subtransfer ) || empty( $subtransfer['is_income'] ) ) {
							continue;
						}

						$sub_asset_id = $subtransfer['asset_id'] ?? self::DEFAULT_ZANO_ASSET_ID;
						if ( null !== $asset_id && '' !== $asset_id && $sub_asset_id !== $asset_id ) {
							continue;
						}

						$asset   = self::ASSET_CONFIGS[ $sub_asset_id ] ?? self::ASSET_CONFIGS[ self::DEFAULT_ZANO_ASSET_ID ];
						$amount += (float) ( ( $subtransfer['amount'] ?? 0 ) / $asset['divisor'] );
					}
				}

				return [
					'paymentId' => $expected,
					'amount'    => $amount,
				];
			}

			return null;
		}

		// Multi-payment list shape: payments: [{paymentId, amount}, ...] (human units).
		if ( isset( $verification['payments'] ) && is_array( $verification['payments'] ) ) {
			foreach ( $verification['payments'] as $entry ) {
				if ( ! is_array( $entry ) ) {
					continue;
				}

				$entry_pid = $entry['paymentId'] ?? ( $entry['payment_id'] ?? '' );
				if ( ! self::payment_id_matches( $entry_pid, $expected ) ) {
					continue;
				}

				if ( null !== $asset_id && '' !== $asset_id && ! empty( $entry['assetId'] ) && $entry['assetId'] !== $asset_id ) {
					continue;
				}

				return [
					'paymentId' => $expected,
					'amount'    => floatval( $entry['amount'] ?? 0 ),
				];
			}

			return null;
		}

		// Legacy flat shape: single tx-wide paymentId + amount.
		$flat_pid = $verification['paymentId'] ?? '';
		if ( self::payment_id_matches( $flat_pid, $expected ) ) {
			return [
				'paymentId' => $expected,
				'amount'    => floatval( $verification['amount'] ?? 0 ),
			];
		}

		return null;
	}

	/**
	 * Compare a raw payment ID value from an API response with an expected
	 * lowercase hex payment ID, tolerating hex-string and uint64 encodings.
	 *
	 * @param mixed  $raw      Raw payment ID value.
	 * @param string $expected Expected 16-char lowercase hex payment ID.
	 * @return bool True if the values represent the same payment ID.
	 */
	private static function payment_id_matches( $raw, $expected ) {
		if ( is_string( $raw ) && hash_equals( $expected, strtolower( $raw ) ) ) {
			return true;
		}

		$normalized = self::normalize_intrinsic_payment_id( $raw );

		return ( '' !== $normalized ) && hash_equals( $expected, $normalized );
	}

	/**
	 * Test the connection to the Zano node.
	 *
	 * @return bool|WP_Error True on success, error object on failure
	 */
	public function test_connection() {
		$this->log( "Testing connection to Zano node: {$this->api_url}" );

		if ( empty( $this->api_url ) ) {
			return new WP_Error( 'missing_api_url', 'API URL not configured' );
		}

		try {
			$post_data = wp_json_encode(
				[
					'jsonrpc' => self::JSONRPC_VERSION,
					'id'      => '1',
					'method'  => 'get_info',
					'params'  => new stdClass(), // Empty object instead of empty array for compatibility.
				]
			);

			$args = [
				'body'    => $post_data,
				'headers' => [
					'Content-Type' => 'application/json',
				],
				'timeout' => self::CONNECTION_TEST_TIMEOUT,
			];

			$wp_response = wp_remote_post( $this->api_url, $args );
			$http_code   = wp_remote_retrieve_response_code( $wp_response );

			if ( is_wp_error( $wp_response ) ) {
				$curl_error = $wp_response->get_error_message();
				$response   = false;
			} else {
				$response   = wp_remote_retrieve_body( $wp_response );
				$curl_error = '';
			}

			if ( $response === false ) {
				$this->log( 'Connection test failed: ' . $curl_error );
				return new WP_Error( 'connection_failed', $curl_error );
			}

			if ( $http_code !== 200 ) {
				$this->log( 'Connection test failed with HTTP code ' . $http_code );
				return new WP_Error( 'api_error', 'API request failed with HTTP code ' . $http_code );
			}

			$this->log( 'Connection test successful!' );
			return true;

		} catch ( Exception $e ) {
			$this->log( 'Exception during connection test: ' . $e->getMessage() );
			return new WP_Error( 'exception', $e->getMessage() );
		}
	}

	/**
	 * Make API request to the Zano node/API.
	 *
	 * @param string $method API method to call.
	 * @param array  $params Parameters for the API call.
	 * @return array|WP_Error API response or error.
	 */
	public function api_request( $method, $params = [] ) {
		if ( empty( $this->api_url ) ) {
			return new WP_Error( 'api_not_configured', 'API URL not configured' );
		}

		// Log the request details.
		$this->log( sprintf( 'Making API request to %s', $this->api_url ) );
		$this->log( sprintf( 'Making API call: %s', $method ) );

		try {
			if ( empty( $params ) || $params === [] ) {
				$params = new stdClass();
			}

			$request_data = wp_json_encode(
				[
					'jsonrpc' => self::JSONRPC_VERSION,
					'id'      => '1',
					'method'  => $method,
					'params'  => $params,
				]
			);

			$args = [
				'body'    => $request_data,
				'headers' => [
					'Content-Type' => 'application/json',
				],
				'timeout' => self::DEFAULT_TIMEOUT,
			];

			$wp_response = wp_remote_post( $this->api_url, $args );
			$http_code   = wp_remote_retrieve_response_code( $wp_response );

			if ( is_wp_error( $wp_response ) ) {
				$curl_error = $wp_response->get_error_message();
				$response   = false;
			} else {
				$response   = wp_remote_retrieve_body( $wp_response );
				$curl_error = '';
			}

			if ( $response === false ) {
				$this->log( 'API request failed: ' . $curl_error );
				return new WP_Error( 'api_error', sprintf( 'API request failed: %s', $curl_error ) );
			}

			if ( $http_code !== 200 ) {
				$this->log( 'API request failed with HTTP code ' . $http_code );
				return new WP_Error( 'api_error', sprintf( 'API request failed with code %s', $http_code ) );
			}

			$data = json_decode( $response, true );

			if ( null === $data ) {
				$this->log( 'API response JSON decoding failed' );
				$this->log( 'API response could not be decoded' );
				return new WP_Error( 'api_error', 'Invalid API response format' );
			}

			if ( isset( $data['error'] ) ) {
				$this->log( 'API returned an error response' );
				return new WP_Error( 'api_error', $data['error']['message'] ?? 'Unknown API error' );
			}

			$this->log( 'API request successful' );
			return $data['result'] ?? [];

		} catch ( Exception $e ) {
			$this->log( sprintf( 'Exception during API request: %s', $e->getMessage() ) );
			return new WP_Error( 'api_error', $e->getMessage() );
		}
	}

	/**
	 * Log debug messages.
	 *
	 * @param string $message Message to log.
	 */
	private function log( $message ) {
		if ( $this->debug ) {

			if ( function_exists( 'wc_get_logger' ) ) {
				$logger = wc_get_logger();
				$logger->debug( $message, [ 'source' => 'zano-payment' ] );
			}
		}
	}

	/**
	 * Get the current ZANO price in USD from MEXC Exchange
	 *
	 * @return float|WP_Error Current ZANO price or error
	 */
	public function get_zano_price() {
		$this->log( 'Getting current ZANO price from MEXC Exchange' );

		try {

			$args = [
				'timeout'   => self::PRICE_API_TIMEOUT,
				'sslverify' => false,
			];

			$wp_response = wp_remote_get( self::MEXC_PRICE_API_URL, $args );
			$http_code   = wp_remote_retrieve_response_code( $wp_response );

			if ( is_wp_error( $wp_response ) ) {
				$curl_error = $wp_response->get_error_message();
				$response   = false;
			} else {
				$response   = wp_remote_retrieve_body( $wp_response );
				$curl_error = '';
			}

			if ( $response === false ) {
				$this->log( 'Price fetch failed: ' . $curl_error );
				return new WP_Error( 'api_error', sprintf( 'Price API request failed: %s', $curl_error ) );
			}

			if ( $http_code !== 200 ) {
				$this->log( 'Price fetch failed with HTTP code ' . $http_code );
				return new WP_Error( 'api_error', sprintf( 'Price API request failed with code %s', $http_code ) );
			}

			$this->log( 'Processing price response' );

			$data = json_decode( $response, true );

			if ( null === $data ) {
				$this->log( 'Price response JSON decoding failed' );
				return new WP_Error( 'api_error', 'Invalid price API response format' );
			}

			if ( isset( $data['price'] ) ) {
				$price = floatval( $data['price'] );
				$this->log( 'Current ZANO price: $' . $price . ' USD' );
				return $price;
			}

			$this->log( 'Price not found in response' );
			return new WP_Error( 'price_not_found', 'Could not find ZANO price in API response' );

		} catch ( Exception $e ) {
			$this->log( 'Exception in get_zano_price: ' . $e->getMessage() );
			return new WP_Error( 'api_error', $e->getMessage() );
		}
	}

	/**
	 * Convert USD amount to ZANO amount.
	 *
	 * @param float $usd_amount Amount in USD.
	 * @param float $buffer_percent Optional buffer percentage (default: 1%).
	 * @return float|WP_Error ZANO amount or error.
	 */
	public function convert_usd_to_zano( $usd_amount, $buffer_percent = self::DEFAULT_BUFFER_PERCENTAGE ) {

		$zano_price = $this->get_zano_price();

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

		$zano_amount = $usd_amount / $zano_price;

		$buffer_multiplier       = 1 + ( $buffer_percent / 100 );
		$zano_amount_with_buffer = $zano_amount * $buffer_multiplier;

		$this->log(
			sprintf(
				'Converted $%f USD to %f ZANO (with %f%% buffer: %f ZANO)',
				$usd_amount,
				$zano_amount,
				$buffer_percent,
				$zano_amount_with_buffer
			)
		);

		return $zano_amount_with_buffer;
	}

	/**
	 * Get transaction details by hash using get_tx_details API.
	 *
	 * @param string $tx_hash Transaction hash.
	 * @return array|WP_Error Transaction details or error.
	 */
	public function get_transaction_details( $tx_hash ) {
		if ( empty( $this->api_url ) ) {
			return new WP_Error( 'missing_api_url', 'API URL not set' );
		}

		$this->log( 'Getting transaction details for hash: ' . $tx_hash );

		try {
			$params = [
				'tx_hash' => $tx_hash,
			];

			$result = $this->api_request( 'get_tx_details', $params );

			if ( is_wp_error( $result ) ) {
				$this->log( 'Transaction details request failed: ' . $result->get_error_message() );
				return $result;
			}

			$this->log( 'Transaction details retrieved for hash: ' . $tx_hash );
			return $result;

		} catch ( Exception $e ) {
			$this->log( 'Exception getting transaction details: ' . $e->getMessage() );
			return new WP_Error( 'api_error', $e->getMessage() );
		}
	}

	/**
	 * Update transaction confirmation status using keeper_block from get_tx_details.
	 *
	 * @param string $tx_hash Transaction hash.
	 * @param int    $payment_id Payment record ID.
	 * @return array|WP_Error Updated confirmation status or error.
	 */
	public function update_transaction_confirmations( $tx_hash, $payment_id ) {
		global $wpdb;

		$this->log( "Updating confirmations for transaction: {$tx_hash}" );

		$current_block_height = $this->get_current_block_height();
		if ( is_wp_error( $current_block_height ) ) {
			$this->log( 'Failed to get current block height: ' . $current_block_height->get_error_message() );
			$current_block_height = 0;
		}

		$table_name     = $wpdb->prefix . AnyCoin_Constants::DB_TABLE_NAME;
		$payment_record = AnyCoin_Database_Manager::get_payment_by_id( (int) $payment_id );

		if ( ! $payment_record ) {
			$error_msg = "Payment record not found for ID: {$payment_id}";
			$this->log( $error_msg );
			return new WP_Error( 'payment_not_found', $error_msg );
		}

		$tx_details = $this->get_transaction_details( $tx_hash );

		$keeper_block  = -1;
		$confirmations = 0;

		if ( ! is_wp_error( $tx_details ) ) {

			$keeper_block = isset( $tx_details['result']['tx_info']['keeper_block'] ) ?
				intval( $tx_details['result']['tx_info']['keeper_block'] ) : -1;

			$this->log(
				sprintf(
					'Transaction details from get_tx_details: keeper_block=%d, current_block_height=%d',
					$keeper_block,
					$current_block_height
				)
			);
		} else {
			$this->log( 'Failed to get transaction details: ' . $tx_details->get_error_message() );
		}

		if ( $keeper_block > 0 && $current_block_height > 0 ) {
			$confirmations = max( 0, $current_block_height - $keeper_block + 1 );
			$this->log(
				sprintf(
					'Transaction confirmed: keeper_block=%d, current_block=%d, confirmations=%d',
					$keeper_block,
					$current_block_height,
					$confirmations
				)
			);
		} else {
			$received_block = isset( $payment_record['received_block'] ) ? intval( $payment_record['received_block'] ) : 0;

			if ( $received_block > 0 && $current_block_height > 0 ) {
				$confirmations = max( 0, $current_block_height - $received_block + 1 );
				$this->log(
					sprintf(
						'Transaction unconfirmed but using received_block: received_block=%d, current_block=%d, confirmations=%d',
						$received_block,
						$current_block_height,
						$confirmations
					)
				);
			} else {
				$this->log( 'Transaction unconfirmed and no received_block - checking recent transactions' );

				$recent_transactions = $this->get_recent_transactions();
				if ( ! is_wp_error( $recent_transactions ) ) {
					foreach ( $recent_transactions as $transaction ) {
						if ( ( $transaction['tx_hash'] ?? '' ) === $tx_hash ) {
							$tx_confirmations = intval( $transaction['confirmations'] ?? 0 );
							$tx_block_height  = intval( $transaction['block_height'] ?? 0 );

							$this->log(
								sprintf(
									'Found transaction in recent transactions: confirmations=%d, block_height=%d',
									$tx_confirmations,
									$tx_block_height
								)
							);

							$confirmations = $tx_confirmations;

							if ( $tx_block_height > 0 ) {
								$keeper_block = $tx_block_height;
							}
							break;
						}
					}
				} else {
					$this->log( 'Failed to get recent transactions: ' . $recent_transactions->get_error_message() );
				}
			}
		}

		$this->log(
			sprintf(
				'Final confirmation calculation: keeper_block=%d, current_block_height=%d, confirmations=%d',
				$keeper_block,
				$current_block_height,
				$confirmations
			)
		);

		$update_data = [
			'current_block' => $current_block_height,
			'confirmations' => $confirmations,
			'updated_at'    => current_time( 'mysql' ),
		];

		if ( $keeper_block > 0 ) {
			$update_data['keeper_block'] = $keeper_block;

			if ( empty( $payment_record['received_block'] ) ) {
				$update_data['received_block'] = $keeper_block;
				$this->log(
					sprintf(
						'Setting received_block to keeper_block=%d for confirmed transaction',
						$keeper_block
					)
				);
			} else {
				$this->log(
					sprintf(
						'Preserving existing received_block=%s for confirmed transaction (keeper_block=%d)',
						$payment_record['received_block'],
						$keeper_block
					)
				);
			}
		} else {
			$this->log(
				sprintf(
					'Transaction still unconfirmed (keeper_block=%d), preserving original received_block=%s',
					$keeper_block,
					$payment_record['received_block'] ?? 'null'
				)
			);

			if ( empty( $payment_record['received_block'] ) ) {
				$this->log( 'WARNING: Transaction is unconfirmed but received_block is not set. This should not happen.' );
			}
		}

		$update_result = AnyCoin_Database_Manager::update_payment( (int) $payment_id, $update_data );

		if ( ! $update_result ) {
			$error_msg = 'Failed to update confirmation data';
			$this->log( $error_msg );
			return new WP_Error( 'db_update_failed', $error_msg );
		}

		$this->log(
			sprintf(
				'Updated confirmations for payment %d: keeper_block=%d, confirmations=%d',
				$payment_id,
				$keeper_block,
				$confirmations
			)
		);

		return [
			'keeper_block'  => $keeper_block,
			'current_block' => $current_block_height,
			'confirmations' => $confirmations,
			'tx_confirmed'  => $keeper_block > 0 || $confirmations > 0,
		];
	}

	/**
	 * Get current blockchain height using the /getheight endpoint
	 *
	 * @return int|WP_Error Current block height or error
	 */
	public function get_current_block_height() {
		if ( empty( $this->api_url ) ) {
			return new WP_Error( 'missing_api_url', 'API URL not set' );
		}

		try {
			$this->log( 'Getting current blockchain height using getheight endpoint' );

			$height_url = str_replace( '/json_rpc', '/getheight', $this->api_url );

			$response = wp_remote_get(
				$height_url,
				[
					'timeout' => 30,
					'headers' => [
						'Content-Type' => 'application/json',
					],
				]
			);

			if ( is_wp_error( $response ) ) {
				$this->log( 'Height request failed: ' . $response->get_error_message() );
				return $response;
			}

			$body = wp_remote_retrieve_body( $response );
			$data = json_decode( $body, true );

			if ( json_last_error() !== JSON_ERROR_NONE ) {
				$error_msg = 'Invalid JSON response from getheight endpoint: ' . json_last_error_msg();
				$this->log( $error_msg );
				return new WP_Error( 'json_decode_error', $error_msg );
			}

			if ( ! isset( $data['height'] ) || ! isset( $data['status'] ) ) {
				$this->log( 'Unexpected response format from blockchain height request' );
				return new WP_Error( 'unexpected_response', 'Unexpected response format from getheight endpoint' );
			}

			if ( $data['status'] !== 'OK' ) {
				$error_msg = 'getheight endpoint returned error status: ' . $data['status'];
				$this->log( $error_msg );
				return new WP_Error( 'api_error', $error_msg );
			}

			$blockchain_height = intval( $data['height'] );
			$this->log( 'Current blockchain height from getheight: ' . $blockchain_height );

			return $blockchain_height;

		} catch ( Exception $e ) {
			$this->log( 'Exception getting blockchain height: ' . $e->getMessage() );
			return new WP_Error( 'api_error', $e->getMessage() );
		}
	}

	/**
	 * Get payment record from database.
	 *
	 * @param int $order_id Order ID.
	 * @return array|null Payment record or null if not found.
	 */
	private function get_payment_record( $order_id ) {
		return AnyCoin_Database_Manager::get_payment_by_order_id( (int) $order_id );
	}

	/**
	 * Build confirmed status response.
	 *
	 * @param array $payment Payment record.
	 * @param int   $order_id Order ID.
	 * @return array Status response.
	 */
	private function build_confirmed_status_response( $payment, $order_id ) {
		$this->log( 'Payment already confirmed for order #' . $order_id );

		return [
			'status'        => 'confirmed',
			'tx_hash'       => $payment['tx_hash'] ?? null,
			'confirmations' => $payment['confirmations'] ?? 0,
			'amount'        => $payment['received_amount'] ?? $payment['amount'],
		];
	}

	/**
	 * Get payment requirements from payment record and settings.
	 *
	 * @param array $payment Payment record.
	 * @return array Payment requirements.
	 */
	private function get_payment_requirements( $payment ) {
		$gateway_settings       = get_option( 'woocommerce_anycoin_payment_settings', [] );
		$required_confirmations = isset( $gateway_settings['confirmations'] )
			? intval( $gateway_settings['confirmations'] )
			: 10;

		$required_amount = ! empty( $payment['asset_amount'] )
			? floatval( $payment['asset_amount'] )
			: floatval( $payment['amount'] );

		$expected_asset_symbol = ! empty( $payment['asset_symbol'] )
			? $payment['asset_symbol']
			: 'ZANO';

		return [
			'amount'        => $required_amount,
			'asset_symbol'  => $expected_asset_symbol,
			'confirmations' => $required_confirmations,
			'asset_id'      => $payment['asset_id'] ?? self::DEFAULT_ZANO_ASSET_ID,
			'payment_id'    => $payment['payment_id'],
		];
	}

	/**
	 * Process transactions to find matching payment.
	 *
	 * @param array $transactions List of transactions.
	 * @param array $payment Payment record.
	 * @param array $requirements Payment requirements.
	 * @param int   $order_id Order ID.
	 * @return array Payment status response.
	 */
	private function process_transactions_for_payment( $transactions, $payment, $requirements, $order_id ) {
		foreach ( $transactions as $transaction ) {
			$result = $this->check_transaction_match( $transaction, $payment, $requirements, $order_id );

			if ( $result !== null ) {
				return $result;
			}
		}

		$this->log( 'No matching transaction found for order #' . $order_id );
		return [ 'status' => 'pending' ];
	}

	/**
	 * Check if a transaction matches the payment requirements.
	 *
	 * @param array $transaction Transaction data.
	 * @param array $payment Payment record.
	 * @param array $requirements Payment requirements.
	 * @param int   $order_id Order ID.
	 * @return array|null Status response or null if no match.
	 */
	private function check_transaction_match( $transaction, $payment, $requirements, $order_id ) {
		$tx_hash         = $transaction['tx_hash'] ?? null;
		$tx_amount       = floatval( $transaction['amount'] ?? 0 );
		$tx_asset_id     = $transaction['asset_id'] ?? self::DEFAULT_ZANO_ASSET_ID;
		$tx_asset_symbol = $transaction['asset_symbol'] ?? 'ZANO';
		$confirmations   = intval( $transaction['confirmations'] ?? 0 );

		$this->log_transaction_details( $tx_hash, $tx_amount, $tx_asset_symbol, $tx_asset_id, $confirmations );

		if ( ! $this->is_asset_match( $payment, $tx_asset_id, $tx_asset_symbol, $tx_hash ) ) {
			return null;
		}

		$expected_payment_id = strtolower( (string) $requirements['payment_id'] );
		$intrinsic_pid       = (string) ( $transaction['payment_id'] ?? '' );

		if ( '' !== $intrinsic_pid ) {
			// HF6 fast path: the node already decrypted the per-output intrinsic
			// payment ID with the view key - no external verification needed.
			if ( ! hash_equals( $expected_payment_id, $intrinsic_pid ) ) {
				$this->log(
					sprintf(
						'Intrinsic payment ID mismatch for transaction %s - expected: %s, got: %s - SKIPPING transaction',
						$tx_hash,
						$expected_payment_id,
						$intrinsic_pid
					)
				);
				return null;
			}

			$this->log(
				sprintf(
					'Intrinsic payment ID verified for transaction %s - payment ID: %s (HF6)',
					$tx_hash,
					$intrinsic_pid
				)
			);

			$payment_verification = [
				'paymentId' => $intrinsic_pid,
				'amount'    => $tx_amount,
				'source'    => 'intrinsic',
			];

			// After HF6 one tx may legitimately pay multiple orders (different
			// PIDs), so only block if this exact tx+PID is claimed elsewhere.
			if ( $this->is_transaction_already_claimed( $tx_hash, (int) $payment['id'], $expected_payment_id ) ) {
				return null;
			}
		} else {
			$payment_verification = $this->verify_payment_id_for_transaction( $tx_hash, $requirements['payment_id'], $tx_asset_id );
			if ( $payment_verification === null ) {
				return null;
			}

			// Legacy tx-wide PID: one tx is one payment, keep the strict guard.
			if ( $this->is_transaction_already_claimed( $tx_hash, (int) $payment['id'] ) ) {
				return null;
			}
		}

		$verified_amount = floatval( $payment_verification['amount'] ?? 0 );
		if ( $verified_amount > 0 ) {
			$tx_amount = $verified_amount;
			$this->log( 'Using verified amount from API for transaction' );
		}

		if ( ! $this->is_amount_match( $tx_amount, $requirements['amount'], $requirements['asset_symbol'], $tx_hash ) ) {
			return null;
		}

		return $this->claim_and_process_transaction( $transaction, $payment, $requirements, $order_id, $payment_verification );
	}

	/**
	 * Log transaction details for debugging.
	 *
	 * @param string $tx_hash Transaction hash.
	 * @param float  $tx_amount Transaction amount.
	 * @param string $tx_asset_symbol Asset symbol.
	 * @param string $tx_asset_id Asset ID.
	 * @param int    $confirmations Number of confirmations.
	 */
	private function log_transaction_details( $tx_hash, $tx_amount, $tx_asset_symbol, $tx_asset_id, $confirmations ) {
		$this->log(
			sprintf(
				'Transaction candidate: hash=%s, amount=%f %s, asset_id=%s, confirmations=%d',
				(string) $tx_hash,
				(float) $tx_amount,
				(string) $tx_asset_symbol,
				(string) $tx_asset_id,
				(int) $confirmations
			)
		);
	}

	/**
	 * Check if asset ID matches payment requirements.
	 *
	 * @param array  $payment Payment record.
	 * @param string $tx_asset_id Transaction asset ID.
	 * @param string $tx_asset_symbol Transaction asset symbol.
	 * @param string $tx_hash Transaction hash.
	 * @return bool True if asset matches or no filter needed.
	 */
	private function is_asset_match( $payment, $tx_asset_id, $tx_asset_symbol, $tx_hash ) {
		// An empty record asset means the default (ZANO); comparing strictly
		// prevents a payment in another asset from satisfying the order.
		$expected_asset_id = ! empty( $payment['asset_id'] ) ? $payment['asset_id'] : self::DEFAULT_ZANO_ASSET_ID;

		if ( $expected_asset_id !== $tx_asset_id ) {
			$this->log(
				sprintf(
					'Asset ID mismatch for transaction %s - expected: %s (%s), got: %s (%s) - SKIPPING transaction',
					$tx_hash,
					$expected_asset_id,
					$payment['asset_symbol'] ?? 'Unknown',
					$tx_asset_id,
					$tx_asset_symbol
				)
			);
			return false;
		}
		return true;
	}

	/**
	 * Verify payment ID for transaction and handle errors.
	 *
	 * Handles both the legacy single-payment response and HF6-style responses
	 * where one transaction may contain multiple payments with different
	 * payment IDs and assets.
	 *
	 * @param string      $tx_hash Transaction hash.
	 * @param string      $expected_payment_id Expected payment ID.
	 * @param string|null $asset_id Expected asset ID (optional).
	 * @return array|null Payment verification result or null if failed.
	 */
	private function verify_payment_id_for_transaction( $tx_hash, $expected_payment_id, $asset_id = null ) {
		$payment_verification = $this->verify_payment_id( $tx_hash );

		if ( is_wp_error( $payment_verification ) ) {
							$this->log(
								'Payment verification failed for transaction ' . $tx_hash . ': ' .
								$payment_verification->get_error_message() . ' - SKIPPING transaction'
							);
			return null;
		}

		$matched_payment = self::extract_payment_for_pid( $payment_verification, $expected_payment_id, $asset_id );

		if ( null === $matched_payment ) {
			$this->log(
				sprintf(
					'Payment ID mismatch for transaction %s - expected: %s, no matching payment found - SKIPPING transaction (this prevents race conditions)',
					$tx_hash,
					$expected_payment_id
				)
			);
			return null;
		}

		$this->log(
			sprintf(
				'Payment ID verified for transaction %s - payment ID: %s matches expected: %s',
				$tx_hash,
				$matched_payment['paymentId'],
				$expected_payment_id
			)
		);

		return array_merge( $payment_verification, $matched_payment );
	}

	/**
	 * Check if transaction is already claimed by another payment record.
	 *
	 * When $payment_id_hex is given (HF6 intrinsic PID match), only the same
	 * tx+PID combination counts as claimed, because a single transaction may
	 * legitimately pay multiple orders with different payment IDs after HF6.
	 *
	 * @param string      $tx_hash Transaction hash.
	 * @param int         $payment_id Current payment record ID.
	 * @param string|null $payment_id_hex Payment ID (hex) to scope the check to.
	 * @return bool True if already claimed.
	 */
	private function is_transaction_already_claimed( $tx_hash, $payment_id, $payment_id_hex = null ) {
		global $wpdb;

		$table_name       = $wpdb->prefix . AnyCoin_Constants::DB_TABLE_NAME;
		$existing_payment = AnyCoin_Database_Manager::is_tx_hash_used_elsewhere( $tx_hash, (int) $payment_id, $payment_id_hex );
		if ( $existing_payment ) {
			$this->log(
				sprintf(
					'CRITICAL ERROR: Transaction %s was already used by another order! This should not happen with proper Payment ID verification.',
					$tx_hash
				)
			);
			return true;
		}

		return false;
	}

	/**
	 * Check if transaction amount matches payment requirements.
	 *
	 * @param float  $tx_amount Transaction amount.
	 * @param float  $required_amount Required amount.
	 * @param string $asset_symbol Asset symbol.
	 * @param string $tx_hash Transaction hash.
	 * @return bool True if amount matches within tolerance.
	 */
	private function is_amount_match( $tx_amount, $required_amount, $asset_symbol, $tx_hash ) {
		$amount_diff  = abs( $tx_amount - $required_amount );
		$amount_match = ( $amount_diff <= $required_amount * self::AMOUNT_TOLERANCE_PERCENTAGE );

		if ( ! $amount_match ) {
			$this->log(
				sprintf(
					'Amount mismatch for transaction %s - expected: %f %s, got: %f %s, difference: %f %s - SKIPPING transaction',
					$tx_hash,
					$required_amount,
					$asset_symbol,
					$tx_amount,
					$asset_symbol,
					$amount_diff,
					$asset_symbol
				)
			);
			return false;
		}

		$this->log(
			sprintf(
				'BOTH Payment ID and Amount verified for transaction %s - Amount: %f %s (required: %f %s, difference: %f %s)',
				$tx_hash,
				$tx_amount,
				$asset_symbol,
				$required_amount,
				$asset_symbol,
				$amount_diff,
				$asset_symbol
			)
		);

		return true;
	}

	/**
	 * Claim and process a matching transaction.
	 *
	 * @param array $transaction Transaction data.
	 * @param array $payment Payment record.
	 * @param array $requirements Payment requirements.
	 * @param int   $order_id Order ID.
	 * @param array $payment_verification Payment verification data.
	 * @return array Status response.
	 */
	private function claim_and_process_transaction( $transaction, $payment, $requirements, $order_id, $payment_verification ) {
		global $wpdb;

		$tx_hash         = $transaction['tx_hash'];
		$tx_amount       = floatval( $payment_verification['amount'] ?? $transaction['amount'] );
		$tx_asset_id     = $transaction['asset_id'] ?? self::DEFAULT_ZANO_ASSET_ID;
		$tx_asset_symbol = $transaction['asset_symbol'] ?? 'ZANO';
		$confirmations   = max( 0, intval( $transaction['confirmations'] ?? 0 ) );
		$tx_payment_id   = $payment_verification['paymentId'] ?? '';

		$claim_result = $this->attempt_transaction_claim( $payment, $transaction, $tx_hash, $tx_amount, $confirmations );

		if ( ! $claim_result ) {
			return [ 'status' => 'pending' ];
		}

		// Record intrinsic verification at claim time: the external decode API
		// cannot re-check intrinsic PIDs later, so the status cron relies on
		// this marker to know the identifier was already verified.
		if ( ( $payment_verification['source'] ?? '' ) === 'intrinsic' ) {
			update_post_meta( (int) $order_id, '_anycoin_verification_method', 'intrinsic_payment_id' );
		}

		$this->log( sprintf( 'Successfully claimed transaction for order #%d', $order_id ) );

		if ( $confirmations >= $requirements['confirmations'] ) {
			return $this->complete_confirmed_payment( $payment, $order_id, $tx_hash, $tx_amount, $tx_asset_id, $tx_asset_symbol, $tx_payment_id, $confirmations );
		} else {
			return $this->handle_detected_payment( $payment, $order_id, $tx_hash, $tx_amount, $tx_asset_id, $tx_asset_symbol, $tx_payment_id, $confirmations, $requirements['confirmations'] );
		}
	}

	/**
	 * Attempt to atomically claim a transaction.
	 *
	 * @param array  $payment Payment record.
	 * @param array  $transaction Transaction data.
	 * @param string $tx_hash Transaction hash.
	 * @param float  $tx_amount Transaction amount.
	 * @param int    $confirmations Number of confirmations.
	 * @return bool True if successfully claimed.
	 */
	private function attempt_transaction_claim( $payment, $transaction, $tx_hash, $tx_amount, $confirmations ) {
		global $wpdb;

		$wpdb->suppress_errors( true );

		$current_blockchain_height = $this->get_current_block_height();
		if ( is_wp_error( $current_blockchain_height ) ) {
			$current_blockchain_height = 0;
		}

		$tx_block_height = $transaction['block_height'] ?? -1;
		$received_block  = ( $tx_block_height > 0 ) ? $tx_block_height : $current_blockchain_height;

		$this->log(
			sprintf(
				'Block info for transaction %s: tx_block_height=%d, current_blockchain_height=%d, received_block=%d',
				$tx_hash,
				$tx_block_height,
				$current_blockchain_height,
				$received_block
			)
		);

		$update_data = [
			'tx_hash'         => $tx_hash,
			'confirmations'   => $confirmations,
			'received_amount' => $tx_amount,
		];

		AnyCoin_Database_Manager::run_table_migrations();
		$update_data['received_block'] = $received_block;
		$update_data['current_block']  = $current_blockchain_height;
		if ( $tx_block_height > 0 ) {
			$update_data['keeper_block'] = $tx_block_height;
		}

		$claim_result = AnyCoin_Database_Manager::claim_transaction_if_unclaimed( (int) $payment['id'], $update_data );

		$db_error = '';

		if ( ! $claim_result ) {
			$this->log(
				sprintf(
					'Database constraint prevented claiming transaction %s for order #%d',
					$tx_hash,
					$payment['order_id']
				)
			);
			return false;
		}

		return true;
	}

	/**
	 * Complete a confirmed payment.
	 *
	 * @param array  $payment Payment record.
	 * @param int    $order_id Order ID.
	 * @param string $tx_hash Transaction hash.
	 * @param float  $tx_amount Transaction amount.
	 * @param string $tx_asset_id Asset ID.
	 * @param string $tx_asset_symbol Asset symbol.
	 * @param string $tx_payment_id Payment ID.
	 * @param int    $confirmations Number of confirmations.
	 * @return array Status response.
	 */
	private function complete_confirmed_payment( $payment, $order_id, $tx_hash, $tx_amount, $tx_asset_id, $tx_asset_symbol, $tx_payment_id, $confirmations ) {
		global $wpdb;

		$this->log( 'Payment confirmed with ' . $confirmations . ' confirmations via transaction verification' );

		$table_name = $wpdb->prefix . AnyCoin_Constants::DB_TABLE_NAME;
		AnyCoin_Database_Manager::update_payment( (int) $payment['id'], [ 'status' => AnyCoin_Constants::STATUS_CONFIRMED ] );

		$this->update_woocommerce_order( $order_id, $tx_payment_id, $tx_amount, $tx_asset_symbol, $tx_hash );

		$saved_confirmations = $this->get_saved_confirmations( $payment['id'] );

		$this->log(
			sprintf(
				'Returning confirmed status: blockchain_confirmations=%d, saved_confirmations=%d',
				$confirmations,
				$saved_confirmations
			)
		);

		return [
			'status'                => 'confirmed',
			'tx_hash'               => $tx_hash,
			'confirmations'         => $saved_confirmations,
			'amount'                => $tx_amount,
			'asset_id'              => $tx_asset_id,
			'asset_symbol'          => $tx_asset_symbol,
			'asset_amount'          => $tx_amount,
			'payment_id'            => $tx_payment_id,
			'matched_by_payment_id' => true,
		];
	}

	/**
	 * Handle detected but unconfirmed payment.
	 *
	 * @param array  $payment Payment record.
	 * @param int    $order_id Order ID.
	 * @param string $tx_hash Transaction hash.
	 * @param float  $tx_amount Transaction amount.
	 * @param string $tx_asset_id Asset ID.
	 * @param string $tx_asset_symbol Asset symbol.
	 * @param string $tx_payment_id Payment ID.
	 * @param int    $confirmations Number of confirmations.
	 * @param int    $required_confirmations Required confirmations.
	 * @return array Status response.
	 */
	private function handle_detected_payment( $payment, $order_id, $tx_hash, $tx_amount, $tx_asset_id, $tx_asset_symbol, $tx_payment_id, $confirmations, $required_confirmations ) {
		global $wpdb;

		$this->log( 'Payment detected but waiting for confirmations: ' . $confirmations . '/' . $required_confirmations );

		$table_name = $wpdb->prefix . AnyCoin_Constants::DB_TABLE_NAME;
		AnyCoin_Database_Manager::update_payment( (int) $payment['id'], [ 'status' => AnyCoin_Constants::STATUS_PROCESSING ] );

		$saved_confirmations = $this->get_saved_confirmations( $payment['id'] );

		$this->log(
			sprintf(
				'Returning detected status: blockchain_confirmations=%d, saved_confirmations=%d',
				$confirmations,
				$saved_confirmations
			)
		);

		return [
			'status'                 => 'detected',
			'tx_hash'                => $tx_hash,
			'confirmations'          => $saved_confirmations,
			'amount'                 => $tx_amount,
			'asset_id'               => $tx_asset_id,
			'asset_symbol'           => $tx_asset_symbol,
			'asset_amount'           => $tx_amount,
			'payment_id'             => $tx_payment_id,
			'matched_by_payment_id'  => true,
			'required_confirmations' => $required_confirmations,
		];
	}

	/**
	 * Update WooCommerce order status.
	 *
	 * @param int    $order_id Order ID.
	 * @param string $payment_id Payment ID.
	 * @param float  $amount Amount.
	 * @param string $asset_symbol Asset symbol.
	 * @param string $tx_hash Transaction hash.
	 */
	private function update_woocommerce_order( $order_id, $payment_id, $amount, $asset_symbol, $tx_hash ) {
		if ( ! function_exists( 'wc_get_order' ) ) {
			return;
		}

		$order = wc_get_order( $order_id );
		if ( ! $order || ! in_array( $order->get_status(), [ 'on-hold', 'pending' ], true ) ) {
			return;
		}

		$order->update_status( 'processing', 'Payment confirmed via Payment ID verification' );
		$order->add_order_note(
			sprintf(
				'Zano payment confirmed via Payment ID verification. Payment ID: %s, Amount: %f %s, Transaction ID: %s',
				$payment_id,
				$amount,
				$asset_symbol,
				$tx_hash
			)
		);
		$order->save();
	}

	/**
	 * Get saved confirmations from database.
	 *
	 * @param int $payment_id Payment ID.
	 * @return int Number of confirmations.
	 */
	private function get_saved_confirmations( $payment_id ) {
		global $wpdb;

		$table_name      = $wpdb->prefix . AnyCoin_Constants::DB_TABLE_NAME;
		$updated_payment = AnyCoin_Database_Manager::get_payment_by_id( (int) $payment_id );

		return $updated_payment ? intval( $updated_payment['confirmations'] ) : 0;
	}
}
