<?php
/**
 * Card Brand Detector.
 *
 * Captures the card brand on card-paid WooCommerce orders so the Chargeback
 * Monitor can compute per-brand chargeback ratios against network thresholds.
 *
 * @package TrustLens
 * @since   1.2.1
 */

defined( 'ABSPATH' ) || exit;

/**
 * Card Brand Detector class.
 *
 * @since 1.2.1
 */
class TrustLens_Card_Brand_Detector {

	const META_BRAND   = '_trustlens_card_brand';
	const META_CARD_TX = '_trustlens_card_transaction';
	const BRANDS       = array( 'visa', 'mastercard', 'amex', 'discover' );

	/**
	 * Normalize a raw brand string to the canonical set.
	 *
	 * @since 1.2.1
	 * @param string $raw Raw brand string from a gateway.
	 * @return string One of: visa, mastercard, amex, discover, other, unknown.
	 */
	public static function normalize_brand( string $raw ): string {
		$raw = strtolower( trim( $raw ) );
		if ( '' === $raw ) {
			return 'unknown';
		}
		$map = array(
			'visa'             => 'visa',
			'mastercard'       => 'mastercard',
			'master_card'      => 'mastercard',
			'american_express' => 'amex',
			'amex'             => 'amex',
			'discover'         => 'discover',
		);
		return $map[ $raw ] ?? 'other';
	}

	/**
	 * Extract the card brand from a Stripe charge payload.
	 *
	 * @since 1.2.1
	 * @param object $payload Stripe charge object or similar shape.
	 * @return string Canonical brand.
	 */
	public static function extract_brand_from_stripe( $payload ): string {
		$raw = $payload->payment_method_details->card->brand ?? '';
		if ( ! is_string( $raw ) || '' === $raw ) {
			return 'unknown';
		}
		return self::normalize_brand( $raw );
	}

	/**
	 * Extract the card brand from a WooPayments payment or dispute payload.
	 *
	 * @since 1.2.1
	 * @param array $payload WooPayments array (nested or flat shape).
	 * @return string Canonical brand.
	 */
	public static function extract_brand_from_woopayments( array $payload ): string {
		$raw = $payload['payment_method_details']['card']['brand']
			?? $payload['brand']
			?? '';
		if ( ! is_string( $raw ) || '' === $raw ) {
			return 'unknown';
		}
		return self::normalize_brand( $raw );
	}

	/**
	 * Register hooks for card-brand capture.
	 *
	 * @since 1.2.1
	 */
	public function register_hooks(): void {
		add_action( 'wc_stripe_payment_complete', array( $this, 'capture_from_stripe_hook' ), 10, 2 );
		add_action( 'woocommerce_payments_payment_authenticated', array( $this, 'capture_from_woopayments_hook' ), 10, 1 );
		add_action( 'woocommerce_payment_complete', array( $this, 'capture_generic' ), 20, 1 );
	}

	/**
	 * Persist brand on an order and mark it as a card transaction.
	 *
	 * @since 1.2.1
	 * @param int    $order_id Order ID.
	 * @param string $brand    Canonical brand.
	 */
	public function set_brand( int $order_id, string $brand ): void {
		$order = wc_get_order( $order_id );
		if ( ! $order ) {
			return;
		}
		$order->update_meta_data( self::META_BRAND, $brand );
		$order->update_meta_data( self::META_CARD_TX, '1' );
		$order->save();
	}

	/**
	 * Get the stored brand for an order.
	 *
	 * @since 1.2.1
	 * @param int $order_id Order ID.
	 * @return string Canonical brand or 'unknown'.
	 */
	public function get_brand( int $order_id ): string {
		$order = wc_get_order( $order_id );
		if ( ! $order ) {
			return 'unknown';
		}
		$brand = (string) $order->get_meta( self::META_BRAND );
		return '' === $brand ? 'unknown' : $brand;
	}

	/**
	 * Stripe payment-complete hook handler.
	 *
	 * @since 1.2.1
	 * @param int   $order_id Order ID.
	 * @param mixed $response Stripe response (object or array).
	 */
	public function capture_from_stripe_hook( $order_id, $response ): void {
		$order_id = absint( $order_id );
		if ( ! $order_id ) {
			return;
		}
		// Deep-convert array payloads so nested structures (payment_method_details
		// -> card -> brand) become objects too; a shallow (object) cast would
		// leave the nested arrays unreachable via object access, always yielding
		// 'unknown'.
		if ( is_array( $response ) ) {
			$decoded  = json_decode( (string) wp_json_encode( $response ) );
			$response = ( null === $decoded ) ? (object) array() : $decoded;
		}
		$brand = self::extract_brand_from_stripe( $response );
		$this->set_brand( $order_id, $brand );
	}

	/**
	 * WooPayments payment-authenticated hook handler.
	 *
	 * @since 1.2.1
	 * @param mixed $order WC_Order or order id, depending on WooPayments version.
	 */
	public function capture_from_woopayments_hook( $order ): void {
		if ( is_numeric( $order ) ) {
			$order = wc_get_order( absint( $order ) );
		}
		if ( ! $order instanceof WC_Order ) {
			return;
		}

		$raw     = (string) $order->get_meta( '_stripe_card_brand' );
		$brand   = '' !== $raw ? self::normalize_brand( $raw ) : 'unknown';

		if ( 'unknown' === $brand ) {
			$payload = $order->get_meta( '_wcpay_charge_response' );
			if ( is_array( $payload ) ) {
				$brand = self::extract_brand_from_woopayments( $payload );
			}
		}

		$this->set_brand( $order->get_id(), $brand );
	}

	/**
	 * Generic payment-complete fallback for other card gateways.
	 *
	 * @since 1.2.1
	 * @param int $order_id Order ID.
	 */
	public function capture_generic( $order_id ): void {
		$order_id = absint( $order_id );
		if ( ! $order_id ) {
			return;
		}
		$order = wc_get_order( $order_id );
		if ( ! $order ) {
			return;
		}

		// Already captured by a more specific handler.
		if ( '' !== (string) $order->get_meta( self::META_BRAND ) ) {
			return;
		}

		$card_gateways = array(
			'stripe',
			'stripe_cc',
			'woocommerce_payments',
			'authorize_net_cim_credit_card',
			'square_credit_card',
		);
		$gateway = $order->get_payment_method();
		if ( ! in_array( $gateway, $card_gateways, true ) ) {
			return;
		}

		$raw   = (string) $order->get_meta( '_stripe_card_brand' );
		$brand = '' !== $raw ? self::normalize_brand( $raw ) : 'unknown';
		$this->set_brand( $order_id, $brand );
	}

	/**
	 * Derive and persist card brand on a historical order based on existing
	 * gateway meta. Idempotent — skips orders that already have brand meta.
	 *
	 * Shared helper used by both the standalone backfill batch and the
	 * Historical Sync loop, so the same logic governs both entry points.
	 *
	 * @since 1.2.1
	 * @param WC_Order $order Order to process.
	 * @return string Result: 'skipped' (already captured), 'updated' (card brand written), or 'marked' (non-card, flagged).
	 */
	public function capture_historical_order( WC_Order $order ): string {
		if ( '' !== (string) $order->get_meta( self::META_BRAND ) ) {
			return 'skipped';
		}

		$card_gateways = array( 'stripe', 'stripe_cc', 'woocommerce_payments' );
		$gateway       = $order->get_payment_method();

		if ( ! in_array( $gateway, $card_gateways, true ) ) {
			// Non-card gateway — mark so we never re-scan this order.
			$order->update_meta_data( self::META_BRAND, 'unknown' );
			$order->update_meta_data( self::META_CARD_TX, '0' );
			$order->save();
			return 'marked';
		}

		$raw   = (string) $order->get_meta( '_stripe_card_brand' );
		$brand = '' !== $raw ? self::normalize_brand( $raw ) : 'unknown';
		$order->update_meta_data( self::META_BRAND, $brand );
		$order->update_meta_data( self::META_CARD_TX, '1' );
		$order->save();
		return 'updated';
	}

	/**
	 * Process one backfill batch. Reads card-paid orders missing brand meta and
	 * derives the brand from existing gateway meta.
	 *
	 * @since 1.2.1
	 * @param int $batch_size Orders per batch.
	 * @return array { processed: int, updated: int, remaining: int }
	 */
	public function backfill_batch( int $batch_size = 50 ): array {
		$orders = wc_get_orders( array(
			'limit'      => $batch_size,
			'status'     => array( 'completed', 'processing', 'refunded' ),
			'meta_query' => array(
				array(
					'key'     => self::META_BRAND,
					'compare' => 'NOT EXISTS',
				),
			),
			'return'     => 'objects',
		) );

		$updated = 0;
		foreach ( $orders as $order ) {
			if ( ! $order instanceof WC_Order ) {
				continue;
			}
			if ( 'updated' === $this->capture_historical_order( $order ) ) {
				++$updated;
			}
		}

		$remaining_ids = wc_get_orders( array(
			'limit'      => -1,
			'status'     => array( 'completed', 'processing', 'refunded' ),
			'meta_query' => array(
				array(
					'key'     => self::META_BRAND,
					'compare' => 'NOT EXISTS',
				),
			),
			'return'     => 'ids',
		) );
		$remaining_count = is_array( $remaining_ids ) ? count( $remaining_ids ) : 0;

		// Backfill retrofits brand onto past orders which changes per-brand
		// ratios and the 12-month trend. Invalidate the Monitor caches so the
		// dashboard and Pro page reflect the new bucketing on the next load.
		if ( $updated > 0 && class_exists( 'TrustLens_Chargeback_Monitor' ) ) {
			TrustLens_Chargeback_Monitor::clear_ratios_cache();
		}

		return array(
			'processed' => count( $orders ),
			'updated'   => $updated,
			'remaining' => $remaining_count,
		);
	}
}
