<?php

declare(strict_types=1);

namespace Australcode\Rewards\Service;

use Australcode\Rewards\Domain\Earning\EarningOverrideResolver;
use Australcode\Rewards\Domain\Settings\Settings;
use Australcode\Rewards\Domain\Settings\SettingsRepository;
use WC_Order;
use WC_Order_Item_Product;
use WC_Product;

defined( 'ABSPATH' ) || exit;

/**
 * Computes how many points an order (or an arbitrary amount) earns.
 *
 * Phase 1.b: a global rate (points_per_currency_unit) applied per line item,
 * each multiplied by its product/category override (FREE). Resolution lives in
 * the EarningOverrideResolver; a multiplier of 0.0 excludes the line. Earning is
 * floored per line and summed. points_for_amount() powers the PDP estimate and
 * accepts an optional product id so the estimate reflects that product's
 * override.
 *
 * The tier and campaign multipliers (Pro) are applied on top of this result by
 * OrderEarningHook, so product/category overrides stack with them.
 *
 * Earning-fairness controls (B8, all FREE): the per-line rounding mode
 * (floor/round/ceil), whether on-sale lines earn, whether the eligible base is the
 * price before or after coupon discounts, and whether the amount paid with redeemed
 * points is excluded from earning ("no points on points").
 */
final class EarningCalculator {

	public function __construct(
		private readonly SettingsRepository $settings,
		private readonly EarningOverrideResolver $overrides,
	) {
	}

	public function points_for_order( WC_Order $order ): int {
		$settings = $this->settings->get();
		if ( ! $settings->earning_enabled ) {
			return 0;
		}

		$before = ( 'before_discounts' === $settings->earning_base );
		$total  = 0;
		foreach ( $order->get_items() as $item ) {
			if ( ! $item instanceof WC_Order_Item_Product ) {
				continue;
			}

			if ( $settings->earning_exclude_sale ) {
				$product = $item->get_product();
				if ( $product instanceof WC_Product && $product->is_on_sale() ) {
					continue;
				}
			}

			$eligible = $before ? (float) $item->get_subtotal() : (float) $item->get_total();
			if ( $settings->earning_include_tax ) {
				$eligible += $before ? (float) $item->get_subtotal_tax() : (float) $item->get_total_tax();
			}
			if ( $eligible <= 0 ) {
				continue;
			}

			$multiplier = $this->overrides->multiplier_for_product( $item->get_product_id() );
			$total     += $this->apply_rounding( $eligible * $settings->points_per_currency_unit * $multiplier, $settings->earning_rounding );
		}

		if ( $settings->earning_exclude_redeemed ) {
			$total -= $this->redeemed_base_points( $order, $settings );
		}

		return max( 0, $total );
	}

	/**
	 * Points for an arbitrary amount (e.g. a product price on the PDP). An
	 * estimate; the authoritative value is computed from the order at earn time.
	 * Pass a product id to fold in that product's earning override.
	 */
	public function points_for_amount( float $amount, int $product_id = 0 ): int {
		$settings = $this->settings->get();
		if ( ! $settings->earning_enabled || $amount <= 0 ) {
			return 0;
		}

		$multiplier = $product_id > 0 ? $this->overrides->multiplier_for_product( $product_id ) : 1.0;
		return $this->apply_rounding( $amount * $settings->points_per_currency_unit * $multiplier, $settings->earning_rounding );
	}

	/**
	 * Base points the customer would otherwise earn on the amount they paid with
	 * redeemed points (the wallet redemption fee), so earning_exclude_redeemed can
	 * remove it — points are not earned on points. Reads the redeemed-points meta
	 * written by RedemptionCartHook and converts it back to a currency amount at the
	 * redemption rate, then to base points at the earning rate.
	 */
	private function redeemed_base_points( WC_Order $order, Settings $settings ): int {
		$redeemed_points = (int) $order->get_meta( '_acrwd_points_redeemed' );
		if ( $redeemed_points <= 0 || $settings->redemption_rate < 1 ) {
			return 0;
		}
		$redeemed_amount = floor( $redeemed_points / $settings->redemption_rate * 100 ) / 100;
		return $this->apply_rounding( $redeemed_amount * $settings->points_per_currency_unit, $settings->earning_rounding );
	}

	/**
	 * Convert a fractional points amount to a whole number using the configured
	 * rounding mode. floor (the default) never awards more than the rate implies.
	 */
	private function apply_rounding( float $value, string $mode ): int {
		return match ( $mode ) {
			'round' => (int) round( $value ),
			'ceil'  => (int) ceil( $value ),
			default => (int) floor( $value ),
		};
	}
}
