<?php
/**
 * Dispute (chargeback) webhook handler.
 *
 * BUSINESS PROBLEM
 * ----------------
 * When a customer files a chargeback with their issuing bank, Stripe
 * sends `charge.dispute.created` and IMMEDIATELY withdraws the disputed
 * funds (plus a $15 dispute fee) from the platform's Stripe balance.
 *
 * Without this handler, the vendor still holds the full transfer amount
 * SPP sent them previously — so the platform is out the charged amount
 * AND the dispute fee AND the vendor share. For a platform doing $100k
 * GMV/month at a typical 0.3% chargeback rate, that's $300/month in
 * silent leakage at minimum.
 *
 * WHAT THIS DOES
 * --------------
 * On `charge.dispute.created`:
 *   1. Resolve the WC order from the charge ID.
 *   2. Reverse vendor transfers proportional to the dispute amount,
 *      using the same processRefund() math as a partial refund.
 *   3. Add a clear order note so admins see what happened.
 *
 * On `charge.dispute.won`:
 *   - Add an order note. Funds are automatically returned by Stripe
 *     to the platform balance. We do NOT re-transfer to the vendor
 *     because the original transfer is already reversed; if the admin
 *     wants to re-pay the vendor they can do so manually via the
 *     Transfers tab Retry action.
 *
 * On `charge.dispute.lost`:
 *   - Add an order note. The reversal we already did is permanent.
 *
 * SAFETY
 * ------
 * Each event is processed exactly once via the same is_transfer_process_done
 * gate the transfer engine uses + a dispute-specific meta marker so a
 * re-delivered dispute webhook doesn't double-reverse.
 *
 * @package SplitPay
 * @since 3.7.5
 */

declare( strict_types=1 );

namespace SplitPay\Integrations\WooCommerce;

use SplitPay\Core\ConnectedAccounts;
use SplitPay\Core\StripeService;
use SplitPay\Core\TransferEngine;
use SplitPay\Core\TransferLogger;
use SplitPay\Integrations\WooCommerce\Adapters\WCOrderAdapter;
use SplitPay\Integrations\WooCommerce\Gateways\WCStripeGateway;

defined( 'ABSPATH' ) || exit;

class DisputeHandler {

	/**
	 * Process a Stripe dispute webhook payload.
	 *
	 * Called from the existing webhook entry point after signature
	 * verification has already passed. Idempotent: re-delivery of the
	 * same event_id on the same order is a no-op.
	 *
	 * @param string $eventType One of charge.dispute.{created,won,lost,closed,funds_withdrawn,funds_reinstated}.
	 * @param object $eventBody json_decode'd webhook body — Stripe Event object.
	 * @return array{status:string, message?:string, reversal_count?:int}
	 */
	public static function handle( string $eventType, $eventBody ): array {
		$disputeObj = $eventBody->data->object ?? null;
		if ( ! $disputeObj ) {
			return [ 'status' => 'error', 'message' => 'Missing dispute object in event' ];
		}

		$chargeId       = (string) ( $disputeObj->charge ?? '' );
		$disputeAmount  = (int)    ( $disputeObj->amount ?? 0 );  // in cents
		$disputeCurrency = strtolower( (string) ( $disputeObj->currency ?? 'usd' ) );
		$reason         = (string) ( $disputeObj->reason ?? 'unspecified' );

		if ( '' === $chargeId ) {
			return [ 'status' => 'error', 'message' => 'Missing charge ID in dispute' ];
		}

		if ( ! class_exists( '\WC_Stripe_Helper' ) ) {
			return [ 'status' => 'error', 'message' => 'WC Stripe helper unavailable; cannot resolve order' ];
		}

		$order = \WC_Stripe_Helper::get_order_by_source_id( $chargeId );
		if ( ! $order ) {
			return [ 'status' => 'order_not_found', 'message' => 'No WC order matches charge ' . $chargeId ];
		}

		$adapter = new WCOrderAdapter( $order, '' );
		$orderId = $adapter->getOrderId();

		// Idempotency: a dispute-specific meta marker so a re-delivered
		// dispute webhook doesn't double-reverse.
		$idemKey = 'spp_dispute_handled_' . md5( ( $eventBody->id ?? '' ) . '|' . $chargeId );
		if ( ! empty( $adapter->getOrderMeta( $idemKey ) ) ) {
			return [ 'status' => 'already_handled', 'message' => 'Event already processed' ];
		}

		switch ( $eventType ) {
			case 'charge.dispute.created':
			case 'charge.dispute.funds_withdrawn':
				// audit P2-2 — dispute observability for the suite spec.
				// Fires BEFORE the reversal call so a partial failure
				// (e.g. createTransferReversal throws) doesn't suppress
				// the counter; the spec asserts increment per terminal
				// event regardless of downstream outcome.
				if ( function_exists( 'do_action' ) ) {
					do_action( 'pts_increment_counter', 'dispute_reversed_funds_withdrawn' );
				}
				return self::reverseDisputedTransfers(
					$adapter, $orderId, $disputeAmount, $disputeCurrency, $reason, $idemKey
				);

			case 'charge.dispute.won':
				$adapter->addOrderNote( sprintf(
					'Split Pay: Dispute on charge %s WON. Stripe has reinstated the disputed funds to your platform balance. Vendor transfers were previously reversed — to re-pay the vendor, use the Retry action on the Transfers tab.',
					$chargeId
				) );
				$adapter->setOrderMeta( $idemKey, '1' );
				if ( function_exists( 'do_action' ) ) {
					do_action( 'pts_increment_counter', 'dispute_won' );
				}
				return [ 'status' => 'noted', 'message' => 'dispute won — order note added' ];

			case 'charge.dispute.lost':
			case 'charge.dispute.closed':
				$adapter->addOrderNote( sprintf(
					'Split Pay: Dispute on charge %s closed (status: %s, reason: %s). Previously reversed vendor transfers are permanent.',
					$chargeId, $eventType, $reason
				) );
				$adapter->setOrderMeta( $idemKey, '1' );
				if ( function_exists( 'do_action' ) ) {
					do_action( 'pts_increment_counter', 'dispute_lost_or_closed' );
				}
				return [ 'status' => 'noted', 'message' => 'dispute closed — order note added' ];

			case 'charge.dispute.funds_reinstated':
				$adapter->addOrderNote( sprintf(
					'Split Pay: Dispute funds REINSTATED on charge %s. Platform balance restored.',
					$chargeId
				) );
				$adapter->setOrderMeta( $idemKey, '1' );
				if ( function_exists( 'do_action' ) ) {
					do_action( 'pts_increment_counter', 'dispute_funds_reinstated' );
				}
				return [ 'status' => 'noted', 'message' => 'funds reinstated — order note added' ];

			default:
				return [ 'status' => 'ignored', 'message' => 'Unhandled dispute sub-type: ' . $eventType ];
		}
	}

	/**
	 * Reverse vendor transfers proportional to the disputed amount.
	 *
	 * Reuses TransferEngine::processRefund() — disputes are semantically
	 * equivalent to refunds for the purposes of vendor reversal math.
	 */
	private static function reverseDisputedTransfers(
		WCOrderAdapter $adapter,
		int $orderId,
		int $disputeAmountCents,
		string $disputeCurrency,
		string $reason,
		string $idemKey
	): array {
		$gateway = new WCStripeGateway();
		$apiKey  = $gateway->getTransferSecretKey();
		if ( '' === $apiKey ) {
			$adapter->addOrderNote( sprintf(
				'Split Pay: Dispute on order %d — cannot reverse vendor transfers because no Stripe API key is configured. Funds were withdrawn from platform balance; reverse vendor transfers manually.',
				$orderId
			) );
			$adapter->setOrderMeta( $idemKey, '1' );
			return [ 'status' => 'error', 'message' => 'no API key' ];
		}

		$stripe   = new StripeService( $apiKey );
		$logger   = new TransferLogger();
		$accounts = new ConnectedAccounts();
		$engine   = new TransferEngine( $stripe, $logger, $accounts );

		// Convert cents -> dollars for the engine's processRefund signature.
		$disputeAmountDollars = $disputeAmountCents / 100;
		$orderTotal           = (float) $adapter->getRawOrder()->get_total();

		$adapter->addOrderNote( sprintf(
			'Split Pay: DISPUTE CREATED (reason: %s) — reversing vendor transfers proportional to disputed amount %s %s of order total %s.',
			$reason,
			number_format( $disputeAmountDollars, 2 ),
			strtoupper( $disputeCurrency ),
			number_format( $orderTotal, 2 )
		) );

		$engine->processRefund( $adapter, $disputeAmountDollars, $orderTotal, 'woocommerce' );

		// Mark this event as handled so a Stripe re-delivery is a no-op.
		$adapter->setOrderMeta( $idemKey, '1' );

		return [ 'status' => 'reversed', 'message' => 'vendor transfers reversed proportional to dispute' ];
	}

	/**
	 * List the dispute event types this handler accepts. Used by the
	 * webhook dispatcher to decide whether to delegate to us.
	 *
	 * @return string[]
	 */
	public static function handledEventTypes(): array {
		return [
			'charge.dispute.created',
			'charge.dispute.won',
			'charge.dispute.lost',
			'charge.dispute.closed',
			'charge.dispute.funds_withdrawn',
			'charge.dispute.funds_reinstated',
		];
	}
}
