<?php
/**
 * PeachPay stripe webhook endpoint.
 *
 * Handles direct webhooks from Stripe using signature verification.
 *
 * @package PeachPay
 */

if ( ! defined( 'PEACHPAY_ABSPATH' ) ) {
	exit;
}

require_once PEACHPAY_ABSPATH . 'core/payments/stripe/utils/class-peachpay-stripe-order-data.php';

/**
 * Verifies a Stripe webhook signature and returns the decoded event array.
 *
 * Implements the Stripe-Signature header verification algorithm documented at
 * https://stripe.com/docs/webhooks/signatures. Replaces the former
 * \Stripe\Webhook::constructEvent() SDK call.
 *
 * Error codes returned in the WP_Error:
 *   'invalid_signature' — malformed Stripe-Signature header (missing t= or v1= parts)
 *   'expired_signature' — webhook timestamp is more than 5 minutes old (replay protection)
 *   'signature_mismatch' — computed HMAC does not match any provided v1 signature
 *   'invalid_payload'   — event body is not valid JSON
 *
 * @param string $payload        Raw (unmodified) request body.
 * @param string $sig_header     Value of the Stripe-Signature HTTP header.
 * @param string $webhook_secret The webhook endpoint signing secret (whsec_...).
 * @return array|WP_Error Decoded event array on success, WP_Error on failure.
 */
function peachpay_stripe_verify_webhook_signature( $payload, $sig_header, $webhook_secret ) {
	// Parse the Stripe-Signature header: t=timestamp,v1=sig1,v1=sig2,...
	$timestamp  = null;
	$signatures = array();

	foreach ( explode( ',', $sig_header ) as $pair ) {
		$parts = explode( '=', $pair, 2 );
		if ( 2 !== count( $parts ) ) {
			continue;
		}
		list( $key, $value ) = $parts;
		if ( 't' === $key ) {
			$timestamp = $value;
		} elseif ( 'v1' === $key ) {
			$signatures[] = $value;
		}
	}

	if ( ! $timestamp || empty( $signatures ) ) {
		return new WP_Error( 'invalid_signature', 'Invalid Stripe-Signature header format.' );
	}

	// Replay protection: reject events older than 5 minutes.
	if ( abs( time() - (int) $timestamp ) > 300 ) {
		return new WP_Error( 'expired_signature', 'Webhook timestamp is too old.' );
	}

	// Compute the expected HMAC-SHA256 signature.
	$signed_payload     = $timestamp . '.' . $payload;
	$expected_signature = hash_hmac( 'sha256', $signed_payload, $webhook_secret );

	// Compare against every v1 signature provided (Stripe may include multiple during key rotation).
	$verified = false;
	foreach ( $signatures as $sig ) {
		if ( hash_equals( $expected_signature, $sig ) ) {
			$verified = true;
			break;
		}
	}

	if ( ! $verified ) {
		return new WP_Error( 'signature_mismatch', 'Stripe webhook signature verification failed.' );
	}

	$event = json_decode( $payload, true );
	if ( ! is_array( $event ) ) {
		return new WP_Error( 'invalid_payload', 'Could not decode Stripe webhook payload.' );
	}

	return $event;
}

/**
 * Stripe webhook handler with direct HMAC signature verification.
 *
 * @param WP_REST_Request $request The webhook request data.
 */
function peachpay_rest_api_stripe_webhook( $request ) {
	// Get the raw payload.
	$payload = $request->get_body();

	// Get the Stripe signature header.
	$sig_header = isset( $_SERVER['HTTP_STRIPE_SIGNATURE'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_STRIPE_SIGNATURE'] ) ) : '';

	// Get the webhook secret.
	$webhook_secret = PeachPay_Stripe_Credentials::get_webhook_secret();

	// If we have a webhook secret, verify the signature using HMAC-SHA256.
	if ( ! empty( $webhook_secret ) && ! empty( $sig_header ) ) {
		$event = peachpay_stripe_verify_webhook_signature( $payload, $sig_header, $webhook_secret );

		if ( is_wp_error( $event ) ) {
			$error_code    = $event->get_error_code();
			$error_message = $event->get_error_message();

			if ( 'invalid_payload' === $error_code ) {
				peachpay_log(
					'error',
					'Stripe Webhook invalid payload - failed.',
					array(
						'event'     => 'stripe.webhook.invalid_payload',
						'component' => 'webhooks',
						'feature'   => 'stripe',
						'operation' => 'invalid_payload',
						'outcome'   => 'failed',
						'method'    => __FUNCTION__,
						'line'      => __LINE__,
						'file'      => __FILE__,
						'context'   => array( 'error_message' => $error_message ),
					)
				);
				wp_send_json_error( 'Invalid payload', 400 );
			} else {
				peachpay_log(
					'error',
					'Stripe Webhook invalid signature - failed.',
					array(
						'event'     => 'stripe.webhook.invalid_signature',
						'component' => 'webhooks',
						'feature'   => 'stripe',
						'operation' => 'invalid_signature',
						'outcome'   => 'failed',
						'method'    => __FUNCTION__,
						'line'      => __LINE__,
						'file'      => __FILE__,
						'context'   => array( 'error_message' => $error_message, 'error_code' => $error_code ),
					)
				);
				wp_send_json_error( 'Invalid signature', 400 );
			}
			return;
		}

		// Mode-mismatch guard: ignore events from the wrong environment.
		$site_mode = ! peachpay_is_test_mode();
		if ( isset( $event['livemode'] ) && (bool) $event['livemode'] !== (bool) $site_mode ) {
			peachpay_log(
				'warning',
				'Stripe Webhook mode mismatch: The recieved Stripe event was ignored because its mode (live/test) does not match the current PeachPay site mode. This helps ensure events are processed only in the appropriate environment and prevents accidental cross-environment actions.',
				array(
					'event'     => 'stripe.webhook.mode_mismatch',
					'component' => 'webhooks',
					'feature'   => 'stripe',
					'operation' => 'mode_validation',
					'outcome'   => 'ignored',
					'method'    => __FUNCTION__,
					'line'      => __LINE__,
					'file'      => __FILE__,
					'context'   => array(
						'event_livemode' => (bool) $event['livemode'],
						'site_mode'      => (bool) $site_mode,
						'event_id'       => ! empty( $event['id'] ) ? (string) $event['id'] : '',
					),
				)
			);
			wp_send_json_success(
				array(
					'ignored'      => true,
					'message'      => sprintf(
						'The Stripe event was not processed because its mode (%s) did not match the current PeachPay site mode (%s). No action taken.',
						( isset( $event['livemode'] ) && $event['livemode'] ) ? 'live' : 'test',
						$site_mode ? 'live' : 'test'
					),
					'webhook_mode' => ( isset( $event['livemode'] ) && $event['livemode'] ) ? 'live' : 'test',
					'current_mode' => $site_mode ? 'live' : 'test',
				)
			);
			return;
		}

		// Idempotency guard: skip duplicate deliveries within 24 hours.
		$event_id      = ! empty( $event['id'] ) ? (string) $event['id'] : '';
		$transient_key = '';
		if ( '' !== $event_id ) {
			$transient_key = 'pp_stripe_event_' . $event_id;
			if ( false !== get_transient( $transient_key ) ) {
				peachpay_log(
					'debug',
					'Stripe Webhook duplicate event skipped - event type: ' . $event['type'] . ' event id: ' . $event_id,
					array(
						'event'     => 'stripe.webhook.duplicate_event_skipped',
						'component' => 'webhooks',
						'feature'   => 'stripe',
						'operation' => 'duplicate_event_skipped',
						'method'    => __FUNCTION__,
						'line'      => __LINE__,
						'file'      => __FILE__,
						'context'   => array(
							'payload' => json_decode( $payload, true ),
						),
					)
				);
				wp_send_json_error(
					array(
						'received'  => true,
						'duplicate' => true,
					)
				);
				return;
			}

			set_transient( $transient_key, true, DAY_IN_SECONDS );
		}

		try {
			peachpay_log(
				'debug',
				'Stripe Webhook verified event received - event type: ' . $event['type'] . ' event id: ' . $event['id'],
				array(
					'event'     => 'stripe.webhook.verified_event_received',
					'component' => 'webhooks',
					'feature'   => 'stripe',
					'operation' => 'verified_event_received',
					'method'    => __FUNCTION__,
					'line'      => __LINE__,
					'file'      => __FILE__,
					'context'   => array(
						'payload' => json_decode( $payload, true ),
					),
				)
			);
			// Process the verified event.
			return peachpay_process_stripe_webhook_event( $event );
		} catch ( \Throwable $e ) {
			if ( '' !== $transient_key ) {
				delete_transient( $transient_key );
			}

			peachpay_log(
				'error',
				'Stripe Webhook processing failed - transient released for retry.',
				array(
					'event'     => 'stripe.webhook.processing_failed',
					'component' => 'webhooks',
					'feature'   => 'stripe',
					'operation' => 'processing_failed',
					'outcome'   => 'failed',
					'method'    => __FUNCTION__,
					'line'      => __LINE__,
					'file'      => __FILE__,
					'context'   => array(
						'event_id'      => $event_id,
						'event_type'    => $event['type'] ?? '',
						'error_message' => $e->getMessage(),
					),
				)
			);

			wp_send_json_error(
				array(
					'received' => false,
					'error'    => 'Webhook processing failed',
				),
				500
			);
			return;
		}
	}

	// Fallback: Handle legacy webhook format (from PeachPay server proxy).
	// This maintains backward compatibility during transition.
	$params    = $request instanceof WP_REST_Request ? $request->get_params() : (array) $request;
	$order_id  = $params['order_id'] ?? null;
	$event_type = $params['type'] ?? 'unknown';

	peachpay_log(
		'info',
		'Stripe Webhook legacy event received - success.',
		array(
			'event'     => 'stripe.webhook.legacy_event_received',
			'component' => 'webhooks',
			'feature'   => 'stripe',
			'operation' => 'legacy_event_received',
			'outcome'   => 'success',
			'method'    => __FUNCTION__,
			'line'      => __LINE__,
			'file'      => __FILE__,
			'context'   => array( 'event_type' => $event_type, 'order_id' => $order_id ),
		)
	);

	$order = wc_get_order( $order_id );
	if ( ! $order ) {
		peachpay_log(
			'error',
			'Stripe Webhook legacy order not found - failed.',
			array(
				'event'     => 'stripe.webhook.legacy_order_not_found',
				'component' => 'webhooks',
				'feature'   => 'stripe',
				'operation' => 'legacy_order_not_found',
				'outcome'   => 'failed',
				'method'    => __FUNCTION__,
				'line'      => __LINE__,
				'file'      => __FILE__,
				'context'   => array( 'order_id' => $order_id ),
			)
		);
		wp_send_json_error( 'Required field "order_id" was invalid or missing', 400 );
		return;
	}

	$reason = '';
	if ( isset( $params['status_message'] ) ) {
		$reason = $params['status_message'];
	}

	$is_the_most_recent_provider   = PeachPay_Stripe_Integration::is_payment_gateway( $order->get_payment_method() );
	$is_most_recent_payment_intent = isset( $params['payment_intent_details'] ) && isset( $params['payment_intent_details']['id'] ) && PeachPay_Stripe_Order_Data::get_payment_intent( $order, 'id' ) === $params['payment_intent_details']['id'];
	$is_dispute                    = isset( $params['type'] ) && ( 'charge.dispute.created' === $params['type'] || 'charge.dispute.closed' === $params['type'] );

	// Block old intent ids from modifying the woocommerce order status, but ignore this restriction for dispute webhooks.
	if ( $is_the_most_recent_provider && ( $is_most_recent_payment_intent || $is_dispute ) ) {
		PeachPay_Stripe::calculate_payment_state( $order, $request->get_params(), $reason );
	}

	wp_send_json_success(
		array(
			'status' => $order->get_status(),
		)
	);
}

/**
 * Process a verified Stripe webhook event.
 *
 * @param array $event The decoded Stripe event array returned by peachpay_stripe_verify_webhook_signature().
 */
function peachpay_process_stripe_webhook_event( $event ) {
	$type     = $event['type'];
	$data     = $event['data']['object'];
	$livemode = $event['livemode'];

	peachpay_log(
		'debug',
		'Stripe Webhook processing event - debug.',
		array(
			'event'     => 'stripe.webhook.processing_event',
			'component' => 'webhooks',
			'feature'   => 'stripe',
			'operation' => 'processing_event',
			'method'    => __FUNCTION__,
			'line'      => __LINE__,
			'file'      => __FILE__,
			'context'   => array( 'event_type' => $type ),
		)
	);
	switch ( $type ) {
		case 'payment_intent.succeeded':
			peachpay_handle_payment_intent_webhook( $data, 'succeeded' , $livemode);
			break;

		case 'payment_intent.payment_failed':
			peachpay_handle_payment_intent_webhook( $data, 'failed' , $livemode);
			break;

		case 'payment_intent.canceled':
			peachpay_handle_payment_intent_webhook( $data, 'canceled' , $livemode);
			break;

		case 'payment_intent.requires_action':
			peachpay_handle_payment_intent_webhook( $data, 'requires_action' , $livemode);
			break;

		case 'payment_intent.processing':
			peachpay_handle_payment_intent_webhook( $data, 'processing' , $livemode);
			break;

		case 'charge.refunded':
			peachpay_handle_charge_refunded_webhook( $data );
			break;

		case 'charge.dispute.created':
			peachpay_handle_dispute_webhook( $data, 'created' );
			break;

		case 'charge.dispute.closed':
			peachpay_handle_dispute_webhook( $data, 'closed' );
			break;

		default:
			// Unhandled event type - just acknowledge receipt.
			peachpay_log(
				'debug',
				'Stripe Webhook unhandled event - debug.',
				array(
					'event'     => 'stripe.webhook.unhandled_event',
					'component' => 'webhooks',
					'feature'   => 'stripe',
					'operation' => 'unhandled_event',
					'method'    => __FUNCTION__,
					'line'      => __LINE__,
					'file'      => __FILE__,
					'context'   => array( 'event_type' => $type ),
				)
			);
			break;
	}

	wp_send_json_success( array( 'received' => true ) );
}

/**
 * Handle payment_intent.* webhook events.
 *
 * Replaces the former SDK charge retrieve ($stripe->charges->retrieve()) with
 * PeachPay_Stripe_HTTP::get(). The $payment_intent parameter is now a plain
 * associative array (the 'data.object' value from the decoded event payload).
 *
 * @param array  $payment_intent The payment_intent data object from the Stripe event (array).
 * @param string $status         The event status suffix (e.g. 'succeeded', 'failed').
 * @param bool   $livemode       Whether the event originated from a live-mode Stripe account.
 */
function peachpay_handle_payment_intent_webhook( $payment_intent, $status, $livemode = false ) {
	// Find the order by payment intent ID.
	$order = peachpay_find_order_by_payment_intent( $payment_intent['id'] );

	if ( ! $order ) {
		peachpay_log(
			'debug',
			'Stripe Webhook payment intent order not found - debug.',
			array(
				'event'     => 'stripe.webhook.payment_intent_order_not_found',
				'component' => 'webhooks',
				'feature'   => 'stripe',
				'operation' => 'payment_intent_order_not_found',
				'method'    => __FUNCTION__,
				'line'      => __LINE__,
				'file'      => __FILE__,
				'context'   => array( 'payment_intent_id' => $payment_intent['id'], 'event_status' => $status ),
			)
		);
		return;
	}

	peachpay_log(
		'info',
		'Stripe Webhook payment intent event - success.',
		array(
			'event'     => 'stripe.webhook.payment_intent_event',
			'component' => 'webhooks',
			'feature'   => 'stripe',
			'operation' => 'payment_intent_event',
			'outcome'   => 'success',
			'method'    => __FUNCTION__,
			'line'      => __LINE__,
			'file'      => __FILE__,
			'context'   => array( 'event_status' => $status, 'order_id' => $order->get_id(), 'order_number' => $order->get_order_number(), 'payment_intent_id' => $payment_intent['id'] ),
		)
	);

	$payment_details = array(
		'payment_intent_details' => array(
			'id'                => $payment_intent['id'],
			'status'            => $payment_intent['status'],
			'amount'            => $payment_intent['amount'],
			'amount_capturable' => $payment_intent['amount_capturable'] ?? 0,
			'currency'          => $payment_intent['currency'],
			'mode'              => $livemode ? 'live' : 'test',
		),
	);

	// GET /v1/charges/{id}?expand[]=balance_transaction — fetch charge details when available.
	if ( ! empty( $payment_intent['latest_charge'] ) ) {
		$mode   = $livemode ? 'live' : 'test';
		$charge = PeachPay_Stripe_HTTP::get(
			'/v1/charges/' . $payment_intent['latest_charge'],
			$mode,
			array( 'expand' => array( 'balance_transaction' ) )
		);
		if ( ! is_wp_error( $charge ) ) {
			$payment_details['charge_details'] = array(
				'id'                  => $charge['id'],
				'status'              => $charge['status'],
				'amount'              => $charge['amount'],
				'amount_refunded'     => $charge['amount_refunded'],
				'refunded'            => $charge['refunded'],
				'balance_transaction' => $charge['balance_transaction'] ?? null,
			);
		} else {
			peachpay_log(
				'debug',
				'Stripe Webhook payment intent charge retrieve failed - debug.',
				array(
					'event'     => 'stripe.webhook.payment_intent_charge_retrieve_failed',
					'component' => 'webhooks',
					'feature'   => 'stripe',
					'operation' => 'payment_intent_charge_retrieve_failed',
					'method'    => __FUNCTION__,
					'line'      => __LINE__,
					'file'      => __FILE__,
					'context'   => array( 'payment_intent_id' => $payment_intent['id'], 'error_message' => $charge->get_error_message() ),
				)
			);
		}
	}

	$reason = '';
	if ( 'failed' === $status && ! empty( $payment_intent['last_payment_error'] ) ) {
		$reason = $payment_intent['last_payment_error']['message'] ?? '';
	}

	PeachPay_Stripe::calculate_payment_state( $order, $payment_details, $reason );
}

/**
 * Handle charge.refunded webhook events.
 *
 * The $charge parameter is now a plain associative array (the decoded
 * 'data.object' value from the Stripe event payload).
 *
 * @param array $charge The charge data object from the Stripe event (array).
 */
function peachpay_handle_charge_refunded_webhook( $charge ) {
	$order = peachpay_find_order_by_charge( $charge['id'] );

	if ( ! $order ) {
		// Try finding by payment intent.
		if ( ! empty( $charge['payment_intent'] ) ) {
			$order = peachpay_find_order_by_payment_intent( $charge['payment_intent'] );
		}
	}

	if ( ! $order ) {
		peachpay_log(
			'debug',
			'Stripe Webhook charge refunded order not found - debug.',
			array(
				'event'     => 'stripe.webhook.charge_refunded_order_not_found',
				'component' => 'webhooks',
				'feature'   => 'stripe',
				'operation' => 'charge_refunded_order_not_found',
				'method'    => __FUNCTION__,
				'line'      => __LINE__,
				'file'      => __FILE__,
				'context'   => array( 'charge_id' => $charge['id'] ),
			)
		);
		return;
	}

	peachpay_log(
		'info',
		'Stripe Webhook charge refunded event - success.',
		array(
			'event'     => 'stripe.webhook.charge_refunded_event',
			'component' => 'webhooks',
			'feature'   => 'stripe',
			'operation' => 'charge_refunded_event',
			'outcome'   => 'success',
			'method'    => __FUNCTION__,
			'line'      => __LINE__,
			'file'      => __FILE__,
			'context'   => array( 'order_id' => $order->get_id(), 'order_number' => $order->get_order_number(), 'charge_id' => $charge['id'] ),
		)
	);

	$payment_details = array(
		'charge_details' => array(
			'id'              => $charge['id'],
			'status'          => $charge['status'],
			'amount'          => $charge['amount'],
			'amount_refunded' => $charge['amount_refunded'],
			'refunded'        => $charge['refunded'],
		),
	);

	// Collect refund line items from the nested refunds list.
	if ( ! empty( $charge['refunds'] ) && ! empty( $charge['refunds']['data'] ) ) {
		$refunds = array();
		foreach ( $charge['refunds']['data'] as $refund ) {
			$refunds[] = array(
				'id'       => $refund['id'],
				'amount'   => $refund['amount'],
				'currency' => $refund['currency'],
			);
		}
		$payment_details['charge_details']['refunds'] = $refunds;
	}

	PeachPay_Stripe::calculate_payment_state( $order, $payment_details );

	// Keep WooCommerce refund records in sync for refunds performed from Stripe Dashboard.
	peachpay_sync_wc_refunds_from_stripe_charge( $order, $charge );
}

/**
 * Create missing WooCommerce refunds for Stripe charge refunds.
 *
 * The $charge parameter is a plain associative array decoded from the Stripe
 * webhook payload (charge.refunded data.object).
 *
 * @param WC_Order $order  The WooCommerce order.
 * @param array    $charge The decoded Stripe charge array from the webhook event.
 */
function peachpay_sync_wc_refunds_from_stripe_charge( $order, $charge ) {
	if ( empty( $charge['refunds'] ) || empty( $charge['refunds']['data'] ) ) {
		peachpay_log(
			'debug',
			'Stripe Webhook charge refunded no refund entries - debug.',
			array(
				'event'     => 'stripe.webhook.charge_refunded_no_refund_entries',
				'component' => 'webhooks',
				'feature'   => 'stripe',
				'operation' => 'charge_refunded_no_refund_entries',
				'method'    => __FUNCTION__,
				'line'      => __LINE__,
				'file'      => __FILE__,
				'context'   => array( 'order_id' => $order->get_id(), 'order_number' => $order->get_order_number(), 'charge_id' => $charge['id'] ),
			)
		);
		return;
	}

	$created_count = 0;
	foreach ( $charge['refunds']['data'] as $refund ) {
		if ( empty( $refund['id'] ) ) {
			continue;
		}

		$existing_refund = peachpay_find_wc_refund_by_stripe_refund_id( $order, $refund['id'], $refund['amount'] ?? null, $refund['currency'] ?? null );
		if ( $existing_refund ) {
			peachpay_log(
				'debug',
				'Stripe webhook refund already processed - skipped.',
				array(
					'event'     => 'stripe.webhook.refund_already_processed',
					'component' => 'webhooks',
					'feature'   => 'refund',
					'operation' => 'refund_sync',
					'method'    => __FUNCTION__,
					'line'      => __LINE__,
					'file'      => __FILE__,
					'context'   => array(
						'stripe_refund_id' => $refund['id'],
						'order_id'         => $order->get_id(),
						'order_number'     => $order->get_order_number(),
					),
				)
			);
			continue;
		}

		$currency      = strtoupper( $refund['currency'] ?? $order->get_currency() );
		$refund_amount = PeachPay_Stripe::display_amount( $refund['amount'], $currency );
		$wc_refund     = wc_create_refund(
			array(
				'order_id' => $order->get_id(),
				'amount'   => $refund_amount,
				'reason'   => sprintf( 'Stripe dashboard refund webhook (Refund ID: %s)', $refund['id'] ),
			)
		);

		if ( is_wp_error( $wc_refund ) ) {
			peachpay_log(
				'error',
				'Stripe Webhook refund sync create WC refund failed - failed.',
				array(
					'event'     => 'stripe.webhook.refund_sync_create_wc_refund_failed',
					'component' => 'webhooks',
					'feature'   => 'stripe',
					'operation' => 'refund_sync_create_wc_refund_failed',
					'outcome'   => 'failed',
					'method'    => __FUNCTION__,
					'line'      => __LINE__,
					'file'      => __FILE__,
					'context'   => array( 'order_id' => $order->get_id(), 'order_number' => $order->get_order_number(), 'stripe_refund_id' => $refund['id'], 'error_message' => $wc_refund->get_error_message() ),
				)
			);
			continue;
		}

		$wc_refund->update_meta_data( '_pp_stripe_refund_id', $refund['id'] );
		$wc_refund->save();
		$created_count++;

		peachpay_log(
			'info',
			'Stripe Webhook refund sync created WC refund - success.',
			array(
				'event'     => 'stripe.webhook.refund_sync_created_wc_refund',
				'component' => 'webhooks',
				'feature'   => 'stripe',
				'operation' => 'refund_sync_created_wc_refund',
				'outcome'   => 'success',
				'method'    => __FUNCTION__,
				'line'      => __LINE__,
				'file'      => __FILE__,
				'context'   => array( 'order_id' => $order->get_id(), 'order_number' => $order->get_order_number(), 'wc_refund_id' => $wc_refund->get_id(), 'stripe_refund_id' => $refund['id'], 'refund_amount' => $refund_amount, 'currency' => $currency ),
			)
		);
	}

	// Follow WooCommerce standard: mark fully-refunded orders as refunded.
	$order = wc_get_order( $order->get_id() );
	if ( $order && $order->get_total() > 0 && floatval( $order->get_total_refunded() ) >= floatval( $order->get_total() ) ) {
		if ( 'refunded' !== $order->get_status() ) {
			$order->update_status( 'refunded', __( 'Order fully refunded via Stripe webhook.', 'peachpay-for-woocommerce' ) );
		}
	} elseif ( $created_count > 0 ) {
		$order->add_order_note(
			sprintf(
				// translators: %d number of refund entries synced from Stripe.
				__( 'Stripe webhook synced %d refund record(s) to WooCommerce.', 'peachpay-for-woocommerce' ),
				$created_count
			)
		);
		$order->save();
	}
}

/**
 * Finds an existing WooCommerce refund by Stripe refund id.
 *
 * @param WC_Order       $order The WooCommerce order.
 * @param string         $stripe_refund_id The Stripe refund id.
 * @param int|float|null $stripe_refund_amount Stripe refund amount in Stripe smallest unit.
 * @param string|null    $stripe_refund_currency Stripe refund currency code.
 * @return WC_Order_Refund|false
 */
function peachpay_find_wc_refund_by_stripe_refund_id( $order, $stripe_refund_id, $stripe_refund_amount = null, $stripe_refund_currency = null ) {
	$order_refunds = $order->get_refunds();

	// Primary idempotency key: exact Stripe refund id.
	foreach ( $order_refunds as $refund ) {
		if ( $stripe_refund_id === $refund->get_meta( '_pp_stripe_refund_id', true ) ) {
			return $refund;
		}
	}

	// Secondary check: some installs may already store the Stripe refund id in refund reason.
	foreach ( $order_refunds as $refund ) {
		$reason = (string) $refund->get_reason();
		if ( '' !== $reason && false !== strpos( $reason, $stripe_refund_id ) ) {
			$refund->update_meta_data( '_pp_stripe_refund_id', $stripe_refund_id );
			$refund->save();
			return $refund;
		}
	}

	// Webhook-only fallback for Woo-admin initiated refunds:
	// if order notes already reference this Stripe refund id, attach it to a likely matching recent Woo refund.
	if ( ! is_numeric( $stripe_refund_amount ) || ! peachpay_order_has_stripe_refund_note( $order, $stripe_refund_id ) ) {
		return false;
	}

	$currency              = strtoupper( $stripe_refund_currency ?? $order->get_currency() );
	$expected_refund_total = (float) PeachPay_Stripe::display_amount( $stripe_refund_amount, $currency );

	foreach ( $order->get_refunds() as $refund ) {
		$refund_total = abs( (float) $refund->get_amount() );
		if ( abs( $refund_total - $expected_refund_total ) >= 0.01 ) {
			continue;
		}

		$created_at = $refund->get_date_created();
		if ( ! $created_at || ( time() - $created_at->getTimestamp() ) > 3600 ) {
			continue;
		}

		$refund->update_meta_data( '_pp_stripe_refund_id', $stripe_refund_id );
		$refund->save();
		return $refund;
	}

	return false;
}

/**
 * Checks whether an order note already references a Stripe refund id.
 *
 * @param WC_Order $order The WooCommerce order.
 * @param string   $stripe_refund_id The Stripe refund id.
 * @return bool
 */
function peachpay_order_has_stripe_refund_note( $order, $stripe_refund_id ) {
	if ( empty( $stripe_refund_id ) ) {
		return false;
	}

	$order_notes = wc_get_order_notes(
		array(
			'order_id' => $order->get_id(),
			'limit'    => 20,
			'orderby'  => 'date_created',
			'order'    => 'DESC',
		)
	);

	foreach ( $order_notes as $note ) {
		if ( isset( $note->content ) && false !== strpos( (string) $note->content, $stripe_refund_id ) ) {
			return true;
		}
	}

	return false;
}

/**
 * Handle charge.dispute.created and charge.dispute.closed webhook events.
 *
 * The $dispute parameter is a plain associative array decoded from the
 * Stripe webhook payload (data.object).
 *
 * @param array  $dispute The decoded Stripe dispute array from the webhook event.
 * @param string $action  The action type ('created' or 'closed').
 */
function peachpay_handle_dispute_webhook( $dispute, $action ) {
	$charge_id = $dispute['charge'];
	$order     = peachpay_find_order_by_charge( $charge_id );

	if ( ! $order ) {
		peachpay_log(
			'debug',
			'Stripe Webhook dispute order not found - debug.',
			array(
				'event'     => 'stripe.webhook.dispute_order_not_found',
				'component' => 'webhooks',
				'feature'   => 'stripe',
				'operation' => 'dispute_order_not_found',
				'method'    => __FUNCTION__,
				'line'      => __LINE__,
				'file'      => __FILE__,
				'context'   => array( 'action' => $action, 'charge_id' => $charge_id ),
			)
		);
		return;
	}

	peachpay_log(
		'info',
		'Stripe Webhook dispute event - success.',
		array(
			'event'     => 'stripe.webhook.dispute_event',
			'component' => 'webhooks',
			'feature'   => 'stripe',
			'operation' => 'dispute_event',
			'outcome'   => 'success',
			'method'    => __FUNCTION__,
			'line'      => __LINE__,
			'file'      => __FILE__,
			'context'   => array( 'action' => $action, 'order_id' => $order->get_id(), 'order_number' => $order->get_order_number(), 'charge_id' => $charge_id, 'dispute_id' => $dispute['id'] ),
		)
	);

	$dispute_details = array(
		'type'       => 'charge.dispute.' . $action,
		'dispute_id' => $dispute['id'],
		'status'     => 'created' === $action ? strtoupper( $dispute['status'] ) : $dispute['status'],
	);

	PeachPay_Stripe::calculate_payment_state( $order, $dispute_details );
}

/**
 * Find a WooCommerce order by Stripe payment intent ID.
 *
 * @param string $payment_intent_id The payment intent ID.
 * @return WC_Order|null The order or null if not found.
 */
function peachpay_find_order_by_payment_intent( $payment_intent_id ) {
	// Search for orders with this payment intent ID.
	$orders = wc_get_orders(
		array(
			'limit'      => 1,
			'meta_key'   => '_pp_stripe_payment_intent_id',
			'meta_value' => $payment_intent_id,
		)
	);

	if ( ! empty( $orders ) ) {
		return $orders[0];
	}

	// Fallback: Search in order meta using the payment intent details structure.
	global $wpdb;
	$order_id = $wpdb->get_var(
		$wpdb->prepare(
			"SELECT post_id FROM {$wpdb->postmeta}
			WHERE meta_key = '_pp_stripe_payment_intent_details'
			AND meta_value LIKE %s
			LIMIT 1",
			'%' . $wpdb->esc_like( $payment_intent_id ) . '%'
		)
	);

	if ( $order_id ) {
		return wc_get_order( $order_id );
	}

	return null;
}

/**
 * Find a WooCommerce order by Stripe charge ID.
 *
 * @param string $charge_id The charge ID.
 * @return WC_Order|null The order or null if not found.
 */
function peachpay_find_order_by_charge( $charge_id ) {
	// Search by transaction ID (charge ID is stored as transaction ID).
	$orders = wc_get_orders(
		array(
			'limit'          => 1,
			'transaction_id' => $charge_id,
		)
	);

	if ( ! empty( $orders ) ) {
		return $orders[0];
	}

	// Fallback: Search in order meta using the charge details structure.
	global $wpdb;
	$order_id = $wpdb->get_var(
		$wpdb->prepare(
			"SELECT post_id FROM {$wpdb->postmeta}
			WHERE meta_key = '_pp_stripe_charge_details'
			AND meta_value LIKE %s
			LIMIT 1",
			'%' . $wpdb->esc_like( $charge_id ) . '%'
		)
	);

	if ( $order_id ) {
		return wc_get_order( $order_id );
	}

	return null;
}
