<?php
/**
 * AnyCoin Transaction Monitor utilities.
 *
 * @package AnyCoin_Payment_Gateway
 */

defined( 'ABSPATH' ) || exit;

/**
 * Zano Transaction Monitor
 *
 * Monitors the Zano blockchain for incoming transactions to process payments.
 */
class AnyCoin_Transaction_Monitor {

	/**
	 * API instance
	 *
	 * @var AnyCoin_API
	 */
	private $api;

	/**
	 * Debug mode
	 *
	 * @var bool
	 */
	private $debug;

	/**
	 * Constructor
	 */
	public function __construct() {
		$gateway_settings = get_option( 'woocommerce_anycoin_payment_settings', [] );
		$this->debug      = isset( $gateway_settings['debug'] ) && $gateway_settings['debug'] === 'yes';

		$api_config = [
			'api_url'            => $gateway_settings['api_url'] ?? '',
			'wallet_address'     => $gateway_settings['wallet_address'] ?? '',
			'view_key'           => $gateway_settings['view_key'] ?? '',
			'payment_id_api_url' => $gateway_settings['payment_id_api_url'] ?? AnyCoin_Constants::PAYMENT_VERIFICATION_API,
			'debug'              => $this->debug,
		];

		$this->api = new AnyCoin_API( $api_config );

		add_action( 'anycoin_check_transactions', [ $this, 'check_transactions' ] );
	}

	/**
	 * Check for new transactions and update pending payments
	 */
	public function check_transactions() {
		global $wpdb;

		$table_name = $wpdb->prefix . AnyCoin_Constants::DB_TABLE_NAME;
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Live monitor query; caching would risk stale payment state.
		$payments_to_check = $wpdb->get_results(
			$wpdb->prepare(
				'SELECT * FROM %i WHERE status IN (%s, %s) ORDER BY created_at ASC',
				$wpdb->prefix . AnyCoin_Constants::DB_TABLE_NAME,
				'pending',
				'processing'
			),
			ARRAY_A
		);

		if ( empty( $payments_to_check ) ) {
			$this->log( 'No payments to check (pending or processing)' );
			return;
		}

		$this->log(
			sprintf(
				'Found %d payments to check (pending: %d, processing: %d)',
				count( $payments_to_check ),
				count(
					array_filter(
						$payments_to_check,
						function ( $p ) {
							return $p['status'] === 'pending';
						}
					)
				),
				count(
					array_filter(
						$payments_to_check,
						function ( $p ) {
							return $p['status'] === 'processing';
						}
					)
				)
			)
		);

		try {
			$transactions = $this->api->get_recent_transactions();

			if ( is_wp_error( $transactions ) ) {
				$this->log( sprintf( 'Error getting transactions: %s', $transactions->get_error_message() ) );
				return;
			}

			if ( empty( $transactions ) ) {
				$this->log( 'No recent transactions found' );
				return;
			}

			$this->log( sprintf( 'Retrieved %d recent transactions', count( $transactions ) ) );

			foreach ( $payments_to_check as $payment ) {
				$this->process_payment( $payment, $transactions );
			}

			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Controlled internal join for expiry; must reflect current order status.
			$expired_payments = $wpdb->get_results(
				$wpdb->prepare(
					'SELECT p.* FROM %i p LEFT JOIN %i o ON p.order_id = o.ID WHERE p.status = %s AND p.created_at < DATE_SUB(NOW(), INTERVAL 15 MINUTE) AND o.post_status IN (%s, %s)',
					$wpdb->prefix . AnyCoin_Constants::DB_TABLE_NAME,
					$wpdb->posts,
					'pending',
					'wc-pending',
					'wc-on-hold'
				),
				ARRAY_A
			);

			if ( ! empty( $expired_payments ) ) {
				$this->log( 'Found ' . count( $expired_payments ) . ' expired pending payments' );

				foreach ( $expired_payments as $payment ) {
					$order = wc_get_order( $payment['order_id'] );
					if ( $order ) {
						$this->log( "Marking expired payment as failed for order #{$payment['order_id']}" );
						$order->update_status( 'failed', 'Payment expired - no transaction received within timeout period' );
						$order->add_order_note( 'Payment automatically marked as failed - no transaction detected within timeout period' );
						$order->save();

						// Update payment status to failed.
						AnyCoin_Database_Manager::update_payment(
							(int) $payment['id'],
							[
								'status'     => 'failed',
								'updated_at' => current_time( 'mysql' ),
							]
						);
					}
				}
			}
		} catch ( Exception $e ) {
			$this->log( 'Exception in check_transactions: ' . $e->getMessage() );
		}
	}

	/**
	 * Process a payment against transactions.
	 *
	 * @param array $payment       Payment record.
	 * @param array $transactions  Recent transactions.
	 */
	private function process_payment( $payment, $transactions ) {
		global $wpdb;
		$table_name = $wpdb->prefix . AnyCoin_Constants::DB_TABLE_NAME;

		$this->log( sprintf( 'Processing payment #%d for order #%d', $payment['id'], $payment['order_id'] ) );

		$required_amount = floatval( $payment['amount'] );

		$gateway_settings       = get_option( 'woocommerce_anycoin_payment_settings', [] );
		$required_confirmations = isset( $gateway_settings['confirmations'] ) ?
			intval( $gateway_settings['confirmations'] ) : 10;

		$this->log( 'Checking for matching transaction amounts' );

		$matched_tx = null;
		foreach ( $transactions as $tx ) {
			$tx_amount       = floatval( $tx['amount'] ?? 0 );
			$tx_hash         = $tx['tx_hash'] ?? 'unknown';
			$tx_asset_id     = $tx['asset_id'] ?? '';
			$tx_asset_symbol = $tx['asset_symbol'] ?? 'Unknown';

			$this->log( sprintf( 'Checking transaction: %s', $tx_hash ) );

			// Only consider transactions in the asset the order expects; an
			// empty record asset means the default (ZANO). Without this, a
			// cheap payment in another asset carrying the right payment ID
			// could satisfy the order.
			$expected_asset_id = ! empty( $payment['asset_id'] ) ? $payment['asset_id'] : AnyCoin_Constants::ZANO_ASSET_ID;
			if ( '' === $tx_asset_id ) {
				$tx_asset_id = AnyCoin_Constants::ZANO_ASSET_ID;
			}
			if ( $tx_asset_id !== $expected_asset_id ) {
				$this->log(
					sprintf(
						'Asset mismatch for transaction %s - order expects %s, tx pays %s - SKIPPING transaction',
						$tx_hash,
						$expected_asset_id,
						$tx_asset_id
					)
				);
				continue;
			}

			$expected_payment_id = strtolower( (string) $payment['payment_id'] );
			$intrinsic_pid       = (string) ( $tx['payment_id'] ?? '' );
			$intrinsic_match     = false;

			if ( '' !== $intrinsic_pid ) {
				// HF6 fast path: intrinsic per-output payment ID decoded by the
				// node 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 - SKIPPING transaction (prevents wrong assignment)',
							$tx_hash
						)
					);
					continue;
				}

				$intrinsic_match      = true;
				$payment_verification = [
					'paymentId' => $intrinsic_pid,
					'amount'    => $tx_amount,
				];

				$this->log( sprintf( 'Intrinsic payment ID verified for transaction %s (HF6)', $tx_hash ) );
			} else {
				$payment_verification = $this->api->verify_payment_id( $tx_hash );
				if ( is_wp_error( $payment_verification ) ) {
					$this->log( 'Payment ID verification failed for transaction ' . $tx_hash . ': ' . $payment_verification->get_error_message() . ' - SKIPPING transaction' );
					continue;
				}

				$matched_payment = AnyCoin_API::extract_payment_for_pid( $payment_verification, $expected_payment_id, $tx_asset_id );

				if ( null === $matched_payment ) {
					$this->log(
						sprintf(
							'Payment ID mismatch for transaction %s - SKIPPING transaction (prevents wrong assignment)',
							$tx_hash
						)
					);
					continue;
				}

				$payment_verification = array_merge( $payment_verification, $matched_payment );

				$this->log( sprintf( 'Payment ID verified for transaction %s - identifiers match', $tx_hash ) );
			}

			// After HF6 one tx may pay multiple orders with different PIDs, so an
			// intrinsic match only conflicts with the same tx+PID combination;
			// legacy tx-wide PIDs keep the strict per-tx guard.
			if ( AnyCoin_Database_Manager::is_tx_hash_used_elsewhere( $tx_hash, (int) $payment['id'], $intrinsic_match ? $expected_payment_id : null ) ) {
				$this->log(
					sprintf(
						'CRITICAL ERROR: Transaction %s already used for another order! Skipping.',
						$tx_hash
					)
				);
				continue;
			}

			$is_fusd = ( $expected_asset_id === AnyCoin_Constants::FUSD_ASSET_ID );

			if ( $is_fusd ) {

				// The node-decoded FUSD amount is authoritative - hold it to
				// the same 2% tolerance as ZANO instead of accepting any amount.
				$fusd_required = ! empty( $payment['asset_amount'] ) ? floatval( $payment['asset_amount'] ) : floatval( $payment['amount'] );
				$fusd_diff     = abs( $tx_amount - $fusd_required );

				if ( $fusd_diff > $fusd_required * 0.02 ) {
					$this->log(
						sprintf(
							'Amount mismatch for FUSD transaction %s - expected: %f FUSD, got: %f FUSD - SKIPPING transaction',
							$tx_hash,
							$fusd_required,
							$tx_amount
						)
					);
					continue;
				}

				$this->log( sprintf( 'FUSD payment and amount verified for transaction %s', $tx_hash ) );

				$matched_tx = array_merge(
					$tx,
					[
						'verified_amount'       => $tx_amount,
						'verified_asset_id'     => $tx_asset_id,
						'verified_asset_symbol' => $tx_asset_symbol,
						'verification_method'   => $intrinsic_match ? 'fusd_intrinsic_payment_id' : 'fusd_payment_id_recent_blocks',
						'intrinsic_match'       => $intrinsic_match,
					]
				);
				break;
			} else {

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

				$amount_diff  = abs( $tx_amount - $required_amount );
				$amount_match = ( $amount_diff <= $required_amount * 0.02 );

				if ( $amount_match ) {
					$this->log( sprintf( 'ZANO payment and amount verified for transaction %s', $tx_hash ) );

					$matched_tx = array_merge(
						$tx,
						[
							'verified_amount'       => $tx_amount,
							'verified_asset_id'     => $tx_asset_id,
							'verified_asset_symbol' => $tx_asset_symbol,
							'verification_method'   => $intrinsic_match ? 'zano_intrinsic_payment_id_amount' : 'zano_payment_id_amount',
							'intrinsic_match'       => $intrinsic_match,
						]
					);
					break;
				} else {
					$this->log(
						sprintf(
							'Amount mismatch for ZANO transaction %s - expected: %f ZANO, got: %f ZANO, difference: %f ZANO - SKIPPING transaction',
							$tx_hash,
							$required_amount,
							$tx_amount,
							$amount_diff
						)
					);
				}
			}
		}

		if ( $matched_tx ) {
			$tx_hash             = $matched_tx['tx_hash'] ?? '';
			$tx_amount           = floatval( $matched_tx['verified_amount'] ?? $matched_tx['amount'] ?? 0 );
			$confirmations       = intval( $matched_tx['confirmations'] ?? 0 );
			$asset_id            = $matched_tx['verified_asset_id'] ?? $matched_tx['asset_id'] ?? '';
			$asset_symbol        = $matched_tx['verified_asset_symbol'] ?? $matched_tx['asset_symbol'] ?? 'Unknown';
			$verification_method = $matched_tx['verification_method'] ?? 'unknown';

			// HF6: an intrinsic-PID match only conflicts with the same tx+PID
			// combination; legacy matches keep the strict per-tx guard.
			$claim_scope_pid = ! empty( $matched_tx['intrinsic_match'] ) ? strtolower( (string) $payment['payment_id'] ) : null;

			if ( AnyCoin_Database_Manager::is_tx_hash_used_elsewhere( $tx_hash, (int) $payment['id'], $claim_scope_pid ) ) {
				$this->log( 'Transaction was already assigned to another order - skipping' );
				return;
			}

			$this->log( sprintf( 'Processing matched transaction with %d confirmations', $confirmations ) );

			if ( $confirmations < 0 ) {
				$confirmations = 0;
			}

			$new_status = ( $confirmations >= $required_confirmations ) ? 'confirmed' : 'processing';

			$tx_block_height           = intval( $matched_tx['block_height'] ?? -1 );
			$current_blockchain_height = $this->api->get_current_block_height();

			$update_data = [
				'tx_hash'         => $tx_hash,
				'confirmations'   => $confirmations,
				'received_amount' => $tx_amount,
				'status'          => $new_status,
				'updated_at'      => current_time( 'mysql' ),
			];

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

			if ( $asset_id && empty( $payment['asset_id'] ) ) {
				$update_data['asset_id'] = $asset_id;
			}
			if ( $asset_symbol && empty( $payment['asset_symbol'] ) ) {
				$update_data['asset_symbol'] = $asset_symbol;
			}
			if ( $tx_amount > 0 && empty( $payment['asset_amount'] ) ) {
				$update_data['asset_amount'] = $tx_amount;
			}

			if ( $new_status === 'confirmed' ) {
				$update_data['completed_at'] = current_time( 'mysql' );
			}

			AnyCoin_Database_Manager::update_payment(
				(int) $payment['id'],
				$update_data
			);

			// 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 ( ! empty( $matched_tx['intrinsic_match'] ) ) {
				update_post_meta( (int) $payment['order_id'], '_anycoin_verification_method', $verification_method );
			}

			if ( $new_status === 'confirmed' ) {
				$this->log( sprintf( 'Transaction confirmed with %d confirmations - updating order status', $confirmations ) );

				$order = wc_get_order( $payment['order_id'] );
				if ( $order ) {

					if ( in_array( $order->get_status(), [ 'on-hold', 'pending', 'processing' ], true ) ) {
						$order->payment_complete( $tx_hash );
						$order->add_order_note(
							sprintf(
								'%s payment confirmed via %s. Amount: %f %s, Transaction ID: %s, Confirmations: %d',
								$asset_symbol,
								'Payment ID + Amount verification',
								$tx_amount,
								$asset_symbol,
								$tx_hash,
								$confirmations
							)
						);

						$order->update_meta_data( '_anycoin_payment_asset_symbol', $asset_symbol );
						$order->update_meta_data( '_anycoin_payment_asset_amount', $tx_amount );
						$order->update_meta_data( '_anycoin_payment_asset_id', $asset_id );
						$order->update_meta_data( '_anycoin_payment_tx_hash', $tx_hash );
						$order->update_meta_data( '_anycoin_verification_method', $verification_method );

						$order->save();

						$this->log( sprintf( 'Order #%d completed with %s payment', $payment['order_id'], $asset_symbol ) );
					}
				}
			} else {
				$this->log(
					sprintf(
						'Transaction detected but waiting for confirmations: %d/%d - status set to processing',
						$confirmations,
						$required_confirmations
					)
				);

				if ( $payment['status'] === 'pending' ) {
					$order = wc_get_order( $payment['order_id'] );
					if ( $order ) {
						$order->add_order_note(
							sprintf(
								'%s payment detected on blockchain. Amount: %f %s, Transaction ID: %s, Confirmations: %d/%d',
								$asset_symbol,
								$tx_amount,
								$asset_symbol,
								$tx_hash,
								$confirmations,
								$required_confirmations
							)
						);
						$order->save();
					}
				}
			}
		} else {
			$this->log( sprintf( 'No matching transaction found for payment #%d (order #%d).', $payment['id'], $payment['order_id'] ) );
		}
	}

	/**
	 * 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' ] );
			}
		}
	}
}
