<?php

declare(strict_types=1);

namespace Australcode\Rewards\Hooks;

use Australcode\Rewards\Service\RedemptionService;
use WC_Cart;
use WC_Order;

defined( 'ABSPATH' ) || exit;

/**
 * Applies a wallet redemption as a negative cart fee (the YITH differentiator:
 * no coupon required), debits the points when the order is placed, and refunds
 * them on cancel/refund/failed.
 *
 * The chosen points live in the WC session until checkout; the debit happens
 * once the order exists (so abandoned carts never lose points). Re-validates the
 * quote against the order subtotal at debit time. Supports classic and Blocks
 * (Store API) checkout. HPOS-safe via the WC_Order CRUD API.
 */
final class RedemptionCartHook {

	private const SESSION_KEY = 'acrwd_redeem_points';
	private const ORDER_META  = '_acrwd_points_redeemed';
	private const REVERT_META = '_acrwd_redemption_reverted';
	private const FEE_ID      = 'acrwd-points-redemption';

	public function __construct( private readonly RedemptionService $redemption ) {
	}

	public function register(): void {
		add_action( 'woocommerce_cart_calculate_fees', array( $this, 'apply_cart_fee' ) );
		add_filter( 'woocommerce_cart_totals_get_fees_from_cart_taxes', array( $this, 'untax_redemption_fee' ), 10, 2 );
		add_action( 'woocommerce_checkout_order_processed', array( $this, 'on_classic_checkout' ), 20, 3 );
		add_action( 'woocommerce_store_api_checkout_order_processed', array( $this, 'on_blocks_checkout' ), 20 );
		add_action( 'woocommerce_order_status_changed', array( $this, 'maybe_refund_redemption' ), 10, 4 );
	}

	public function set_session_points( int $points ): void {
		$session = WC()->session;
		if ( null !== $session ) {
			$session->set( self::SESSION_KEY, max( 0, $points ) );
		}
	}

	public function clear_session(): void {
		$session = WC()->session;
		if ( null !== $session ) {
			$session->__unset( self::SESSION_KEY );
		}
	}

	public function session_points(): int {
		$session = WC()->session;
		if ( null === $session ) {
			return 0;
		}
		return (int) $session->get( self::SESSION_KEY, 0 );
	}

	public function apply_cart_fee( WC_Cart $cart ): void {
		if ( is_admin() && ! wp_doing_ajax() ) {
			return;
		}
		$user_id = get_current_user_id();
		if ( $user_id <= 0 ) {
			return;
		}
		$points = $this->session_points();
		if ( $points <= 0 ) {
			return;
		}

		$quote = $this->redemption->quote( $user_id, $points, (float) $cart->get_subtotal() );
		if ( ! $quote->valid ) {
			return;
		}

		// Registered through the fees API (not the legacy add_fee) so the fee carries a
		// stable, locale-independent id we can match in untax_redemption_fee(). The legacy
		// WC_Cart::add_fee() derives the id from sanitize_title() of the translated name.
		$cart->fees_api()->add_fee(
			array(
				'id'      => self::FEE_ID,
				'name'    => __( 'Points redemption', 'australcode-points-rewards' ),
				'amount'  => -$quote->amount,
				'taxable' => false,
			)
		);
	}

	/**
	 * Forces zero tax on the redemption fee.
	 *
	 * WooCommerce splits tax proportionally across all taxable items for ANY negative
	 * fee, ignoring the fee's `taxable` flag (WC_Cart_Totals::get_fees_from_cart() only
	 * honours that flag for positive fees). In a tax-inclusive store that turns a $N
	 * redemption into an $N×(1+tax) discount. We zero the tax on our own fee (matched by
	 * id) so the discount is the exact points value: the points settle part of the
	 * tax-inclusive total like a payment, without reducing the products' taxable base —
	 * which is also the correct fiscal treatment for loyalty redemption. Other plugins'
	 * fees are left untouched.
	 *
	 * @param array<int|string,float> $taxes Tax amounts WooCommerce computed for this fee, keyed by tax rate id.
	 * @param object                  $fee   Normalised fee line; `$fee->object` is the WC_Cart_Fee.
	 * @return array<int|string,float>
	 */
	public function untax_redemption_fee( array $taxes, object $fee ): array {
		if ( isset( $fee->object->id ) && self::FEE_ID === $fee->object->id ) {
			return array();
		}
		return $taxes;
	}

	/**
	 * @param array<string,mixed> $posted_data
	 */
	public function on_classic_checkout( int $order_id, array $posted_data, WC_Order $order ): void {
		unset( $order_id, $posted_data );
		$this->process_order_redemption( $order );
	}

	public function on_blocks_checkout( WC_Order $order ): void {
		$this->process_order_redemption( $order );
	}

	private function process_order_redemption( WC_Order $order ): void {
		$user_id = $order->get_customer_id();
		if ( $user_id <= 0 ) {
			return;
		}
		if ( '' !== (string) $order->get_meta( self::ORDER_META ) ) {
			return;
		}

		$points = $this->session_points();
		if ( $points <= 0 ) {
			return;
		}

		$quote = $this->redemption->quote( $user_id, $points, (float) $order->get_subtotal() );
		if ( ! $quote->valid ) {
			return;
		}

		$this->redemption->redeem( $user_id, $quote->points, $order->get_id() );
		$order->update_meta_data( self::ORDER_META, (string) $quote->points );
		$order->save();
		$this->clear_session();
	}

	public function maybe_refund_redemption( int $order_id, string $status_from, string $status_to, WC_Order $order ): void {
		unset( $order_id, $status_from );
		if ( ! in_array( $status_to, array( 'cancelled', 'refunded', 'failed' ), true ) ) {
			return;
		}

		$user_id = $order->get_customer_id();
		if ( $user_id <= 0 ) {
			return;
		}

		$points = (int) $order->get_meta( self::ORDER_META );
		if ( $points <= 0 ) {
			return;
		}
		if ( '' !== (string) $order->get_meta( self::REVERT_META ) ) {
			return;
		}

		$this->redemption->refund_redemption( $user_id, $points, $order->get_id() );
		$order->update_meta_data( self::REVERT_META, current_time( 'mysql', true ) );
		$order->save();
	}
}
