<?php
/**
 * Decline_Hooks — subscribes to order-failed + gateway-specific webhook
 * events and records a decline event in the velocity table.
 *
 * Strategy: when the gate evaluates a checkout request, stash the
 * fingerprint_hash on the WC session. When WooCommerce creates a new
 * order, transfer the session fingerprint to the order's _tl_fingerprint_hash
 * meta. Later, when the payment fails, look up the meta and record a
 * decline event against that fingerprint.
 *
 * @package TrustLens
 * @since   1.2.2
 */

defined( 'ABSPATH' ) || exit;

class TrustLens_Decline_Hooks {

	public const ORDER_META_FP_KEY        = '_tl_fingerprint_hash';
	public const ORDER_META_SERVER_FP_KEY = '_tl_server_fingerprint_hash';
	public const SESSION_FP_KEY           = '_tl_fingerprint_hash';
	public const SESSION_SERVER_FP_KEY    = '_tl_server_fingerprint_hash';

	public function register_hooks(): void {
		add_action( 'trustlens/gate_evaluated', array( $this, 'stash_fingerprint_on_session' ), 20, 2 );
		add_action( 'woocommerce_new_order', array( $this, 'transfer_fingerprint_to_order_meta' ), 10, 2 );
		add_action( 'woocommerce_order_status_failed', array( $this, 'on_order_failed' ), 10, 2 );
		add_action( 'wc_gateway_stripe_process_payment_error', array( $this, 'on_stripe_process_error' ), 10, 2 );
		add_action( 'wcpay_payment_failed', array( $this, 'on_wcpay_failed' ), 10, 2 );
	}

	public function stash_fingerprint_on_session( TrustLens_Gate_Context $ctx, ?TrustLens_Gate_Block $block ): void {
		if ( null !== $block ) {
			return; // Request was blocked, no order will be created.
		}
		if ( ! class_exists( 'WC' ) || ! WC()->session ) {
			return;
		}
		WC()->session->set( self::SESSION_FP_KEY, $ctx->fingerprint_hash );
		// Also stash the server-fallback hash so a rotating-client attacker is
		// still trackable across requests at decline-record time.
		WC()->session->set( self::SESSION_SERVER_FP_KEY, $ctx->server_fingerprint_hash );
	}

	public function transfer_fingerprint_to_order_meta( int $order_id, WC_Order $order ): void {
		if ( ! class_exists( 'WC' ) || ! WC()->session ) {
			return;
		}
		$fp = (string) WC()->session->get( self::SESSION_FP_KEY, '' );
		if ( '' === $fp ) {
			return;
		}
		$order->update_meta_data( self::ORDER_META_FP_KEY, $fp );
		$server_fp = (string) WC()->session->get( self::SESSION_SERVER_FP_KEY, '' );
		if ( '' !== $server_fp && $server_fp !== $fp ) {
			$order->update_meta_data( self::ORDER_META_SERVER_FP_KEY, $server_fp );
		}
		$order->save();
	}

	/**
	 * Record a decline event under the primary fingerprint, AND under the
	 * server-fallback fingerprint when they differ. Recording under both lets
	 * the velocity detector catch attackers who rotate the JavaScript-side
	 * client hash per request — the server hash (IP + UA + Accept-Language)
	 * stays stable across the rotation and accumulates declines.
	 */
	private function record_decline_dual( WC_Order $order, string $code ): void {
		$fp = (string) $order->get_meta( self::ORDER_META_FP_KEY );
		if ( '' === $fp ) {
			return;
		}
		$recorder = new TrustLens_Velocity_Recorder();
		$recorder->record_decline( $fp, $code, $order->get_id() );

		$server_fp = (string) $order->get_meta( self::ORDER_META_SERVER_FP_KEY );
		if ( '' !== $server_fp && $server_fp !== $fp ) {
			$recorder->record_decline( $server_fp, $code, $order->get_id() );
		}
	}

	public function on_order_failed( int $order_id, $order ): void {
		if ( ! ( $order instanceof WC_Order ) ) {
			return;
		}
		$this->record_decline_dual( $order, 'order_status_failed' );
	}

	public function on_stripe_process_error( $error, $order ): void {
		if ( ! ( $order instanceof WC_Order ) ) {
			return;
		}
		$code = 'stripe_error';
		if ( is_array( $error ) && isset( $error['code'] ) ) {
			$code = (string) $error['code'];
		}
		$this->record_decline_dual( $order, $code );
	}

	public function on_wcpay_failed( $order_id, $error_code ): void {
		$order = wc_get_order( $order_id );
		if ( ! $order ) {
			return;
		}
		$code = $error_code ? (string) $error_code : 'wcpay_failed';
		$this->record_decline_dual( $order, $code );
	}

	/**
	 * Process a raw Stripe webhook payload. Used when the plugin receives
	 * the webhook directly (not via the official Stripe plugin's action).
	 * Fingerprint is recovered from the payment_intent metadata, which we
	 * must populate at payment-intent creation time (handled by Stripe
	 * integration code — not in scope for this task).
	 *
	 * @param array $payload Decoded Stripe webhook event.
	 */
	public function handle_stripe_payment_failed( array $payload ): void {
		if ( ( $payload['type'] ?? '' ) !== 'payment_intent.payment_failed' ) {
			return;
		}
		$object = $payload['data']['object'] ?? array();
		$fp     = $object['metadata']['tl_fingerprint_hash'] ?? '';
		if ( empty( $fp ) ) {
			return;
		}
		$code = $object['last_payment_error']['code'] ?? 'stripe_webhook_failed';
		$recorder = new TrustLens_Velocity_Recorder();
		$recorder->record_decline( (string) $fp, (string) $code, null );
	}
}
