<?php

declare(strict_types=1);

namespace Australcode\Rewards\Hooks;

use Australcode\Rewards\Domain\Settings\SettingsRepository;
use Australcode\Rewards\Domain\Wallet\CustomerBlock;
use Australcode\Rewards\Domain\Wallet\TransactionType;
use Australcode\Rewards\Service\EarningCalculator;
use Australcode\Rewards\Service\PointsExpiry;
use Australcode\Rewards\Service\RoleEarningResolver;
use Australcode\Rewards\Service\WalletService;
use WC_Order;

defined( 'ABSPATH' ) || exit;

/**
 * Grants points when an order reaches any configured trigger status
 * (completed by default; processing optional, both selectable) and reverts them
 * on cancel/refund/failed. Both operations are idempotent via order meta.
 *
 * Guest orders (no customer id) are skipped in Phase 1 — guest tracking is
 * Phase 7. HPOS-safe: uses the WC_Order CRUD API for meta.
 */
final class OrderEarningHook {

	private const AWARDED_META  = '_acrwd_points_awarded';
	private const REVERTED_META = '_acrwd_points_reverted';

	public function __construct(
		private readonly WalletService $wallet,
		private readonly EarningCalculator $calculator,
		private readonly SettingsRepository $settings,
		private readonly RoleEarningResolver $roles,
		private readonly CustomerBlock $blocks,
	) {
	}

	public function register(): void {
		add_action( 'woocommerce_order_status_changed', array( $this, 'on_status_changed' ), 10, 4 );
		add_filter( 'acrwd_order_earned_points', array( $this, 'filter_earned_points' ), 10, 2 );
	}

	/**
	 * Public read API (the `acrwd_order_earned_points` filter) for the points an
	 * order earned: the authoritative ledger amount once awarded, otherwise a
	 * preview of what it will earn. Lets a theme show the real figure on the
	 * thank-you page instead of guessing. The incoming default is ignored
	 * whenever the plugin can answer.
	 */
	public function filter_earned_points( int $fallback, WC_Order $order ): int {
		unset( $fallback );

		$awarded = $this->wallet->points_earned_for_order( $order->get_id() );
		if ( $awarded > 0 ) {
			return $awarded;
		}

		return $this->points_for_order_preview( $order );
	}

	/**
	 * The points an order would earn: base per-line earning with the per-role
	 * multiplier (Free) applied, then any additional multipliers stacked on via the
	 * `acrwd_earning_multiplier` filter (the Pro VIP-tier and campaign services hook
	 * it; 1.0x / passthrough when none). Pure read — no ledger write, no meta.
	 * Shared by maybe_award() and the preview path of filter_earned_points().
	 */
	public function points_for_order_preview( WC_Order $order ): int {
		$base = $this->calculator->points_for_order( $order );
		if ( $base <= 0 ) {
			return 0;
		}

		$user_id = $order->get_customer_id();
		$points  = $base;
		if ( $user_id > 0 ) {
			$points = $this->roles->apply_multiplier( $user_id, $points );
		}

		/**
		 * Stack additional earning multipliers on top of the base + per-role amount.
		 * The Pro VIP-tier (priority 10) and campaign (priority 20) services hook
		 * this; in the free WP.org build they are stripped, so no listener runs and
		 * the amount passes through unchanged. Decoupling via a filter (instead of
		 * typed constructor deps) is what lets the free build drop the Pro code
		 * without fataling. Also a public extension point for third parties.
		 *
		 * @param int       $points  Points after base + per-role multiplier.
		 * @param int       $user_id Customer id (0 for guests).
		 * @param \WC_Order $order   The order being evaluated.
		 */
		$points = (int) apply_filters( 'acrwd_earning_multiplier', $points, $user_id, $order );

		/**
		 * Flat bonus points added on top of the multiplied amount (B1 points-effect
		 * earning rules). The Pro rule engine is the only listener; the free WP.org
		 * build is stripped, so 0 passes through. Also a public extension point.
		 *
		 * @param int       $bonus   Always 0 by default.
		 * @param int       $user_id Customer id (0 for guests).
		 * @param \WC_Order $order   The order being evaluated.
		 */
		$bonus = (int) apply_filters( 'acrwd_earning_bonus_points', 0, $user_id, $order );

		return max( 0, $points + $bonus );
	}

	public function on_status_changed( int $order_id, string $status_from, string $status_to, WC_Order $order ): void {
		unset( $order_id, $status_from );

		if ( $this->settings->get()->triggers_on( $status_to ) ) {
			$this->maybe_award( $order );
			return;
		}

		if ( in_array( $status_to, array( 'cancelled', 'refunded', 'failed' ), true ) ) {
			$this->maybe_revert( $order );
		}
	}

	private function maybe_award( WC_Order $order ): void {
		$user_id = $order->get_customer_id();
		if ( $user_id <= 0 ) {
			return;
		}
		// A blocked customer earns nothing on their orders (B6). The order is left
		// un-flagged, so points are awarded normally if they are later unblocked and
		// the order transitions through a trigger status again.
		if ( $this->blocks->is_blocked( $user_id ) ) {
			return;
		}
		if ( '' !== (string) $order->get_meta( self::AWARDED_META ) ) {
			return;
		}

		// Base per-line earning with the role (Free), VIP tier and active campaign
		// multipliers stacked on top (the last two are 1.0x when Free / none
		// active). Shared with the public preview so award and preview never drift.
		$points = $this->points_for_order_preview( $order );
		if ( $points <= 0 ) {
			return;
		}

		$expires_at = PointsExpiry::expires_at( $this->settings->get()->points_expiration_months );

		$this->wallet->credit(
			$user_id,
			$points,
			TransactionType::Earn,
			/* translators: %d: order number. */
			sprintf( 'Earned from order #%d', $order->get_id() ),
			$order->get_id(),
			$expires_at
		);

		$order->update_meta_data( self::AWARDED_META, current_time( 'mysql', true ) );
		$order->save();

		/**
		 * Fires after an order's points are awarded. The Pro VIP-tier service hooks
		 * this to promote the customer if the new lifetime total crossed a tier
		 * threshold; stripped (no listener) in the free WP.org build.
		 *
		 * @param int       $user_id Customer id.
		 * @param \WC_Order $order   The order just awarded.
		 */
		do_action( 'acrwd_points_earned', $user_id, $order );
	}

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

		$reverted = $this->wallet->revert_order( $order->get_id(), $user_id );
		if ( $reverted > 0 ) {
			$order->update_meta_data( self::REVERTED_META, current_time( 'mysql', true ) );
			$order->save();
		}
	}
}
