<?php
/**
 * Cart Recovery — Coupon-redemption Listener
 *
 * Attribution layer. The Phase 1 service already marks recovery rows
 * as `recovered` via email match on `woocommerce_thankyou` — a
 * reasonable heuristic, but it also counts orders the visitor would
 * have placed anyway. This listener supplements that heuristic with
 * hard proof: if the order carries a coupon code that lives in our
 * recovery table, we KNOW it came from our mail, regardless of email.
 *
 * Runs on both the thankyou path (fast confirmation) and on
 * order-status transitions to `processing` / `completed` (catches
 * orders that skip thankyou, e.g. offline / admin-created orders).
 *
 * Idempotent — `mark_recovered` on an already-recovered row is a no-op
 * at the repository level (update sets same values), and we double-
 * check status before re-marking to avoid bumping `recovered_at`
 * timestamps on late hooks.
 *
 * @package Niwa_AI
 * @since   0.12.0
 */

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

class Niwa_AI_Cart_Recovery_Coupon_Listener {

	public function __construct(
		private readonly Niwa_AI_Cart_Recovery_Repository $repository
	) {}

	public function register_hooks(): void {
		add_action( 'woocommerce_thankyou', array( $this, 'handle_order_event' ), 30, 1 );
		add_action( 'woocommerce_order_status_processing', array( $this, 'handle_order_event' ), 30, 1 );
		add_action( 'woocommerce_order_status_completed', array( $this, 'handle_order_event' ), 30, 1 );
	}

	/**
	 * Main entry fired by multiple WC hooks. Resolves the order,
	 * inspects its coupons, and marks the owning recovery row as
	 * recovered when there's a match.
	 *
	 * @param int|mixed $order_id Woo order id (some hooks pass null/string).
	 */
	public function handle_order_event( $order_id ): void {
		$order_id = (int) $order_id;
		if ( $order_id <= 0 || ! function_exists( 'wc_get_order' ) ) {
			return;
		}

		$order = wc_get_order( $order_id );
		if ( ! is_object( $order ) ) {
			return;
		}

		$codes = $this->extract_coupon_codes( $order );
		if ( empty( $codes ) ) {
			return;
		}

		foreach ( $codes as $code ) {
			$row = $this->repository->find_by_coupon_code( $code );
			if ( ! is_array( $row ) ) {
				continue;
			}

			$id     = (int) ( $row['id'] ?? 0 );
			$status = (string) ( $row['status'] ?? '' );
			if ( $id <= 0 ) {
				continue;
			}

			// Skip if already terminal — the first hook already recorded it.
			if ( Niwa_AI_Cart_Recovery_Repository::STATUS_RECOVERED === $status ) {
				continue;
			}

			$this->repository->mark_recovered( $id, $order_id );

			/**
			 * Fires after a cart-recovery row is attributed to an order
			 * via coupon match. Distinct from the email-match path in
			 * the Phase 1 service — this fires ONLY on coupon proof.
			 */
			do_action( 'niwa_ai_cart_recovery_attributed_by_coupon', $id, $order_id, $code );
		}
	}

	/**
	 * Best-effort coupon-code extraction. WC's API surface changed
	 * between versions: modern releases expose `get_coupon_codes()`,
	 * older ones surface only the used-coupons collection with
	 * objects carrying `get_code()`. Both paths are covered.
	 *
	 * @return array<int, string>
	 */
	private function extract_coupon_codes( $order ): array {
		$codes = array();

		if ( method_exists( $order, 'get_coupon_codes' ) ) {
			$raw = $order->get_coupon_codes();
			if ( is_array( $raw ) ) {
				foreach ( $raw as $code ) {
					$code = trim( (string) $code );
					if ( '' !== $code ) {
						$codes[] = $code;
					}
				}
			}
		}

		if ( empty( $codes ) && method_exists( $order, 'get_used_coupons' ) ) {
			$raw = $order->get_used_coupons();
			if ( is_array( $raw ) ) {
				foreach ( $raw as $code ) {
					$code = trim( (string) $code );
					if ( '' !== $code ) {
						$codes[] = $code;
					}
				}
			}
		}

		if ( empty( $codes ) && method_exists( $order, 'get_items' ) ) {
			try {
				$items = $order->get_items( 'coupon' );
				if ( is_array( $items ) ) {
					foreach ( $items as $item ) {
						if ( is_object( $item ) && method_exists( $item, 'get_code' ) ) {
							$code = trim( (string) $item->get_code() );
							if ( '' !== $code ) {
								$codes[] = $code;
							}
						}
					}
				}
			} catch ( \Throwable $e ) {
				// Swallow — every hook point has other extraction paths.
			}
		}

		return array_values( array_unique( $codes ) );
	}
}
