<?php
/**
 * BookiFlex Core
 *
 * @package    BookiFlex
 * @subpackage Core
 * @author     BookiFlex
 * @copyright  Copyright (c) 2025 BookiFlex
 * @license    GPL-2.0-or-later
 *
 * @link       https://bookiflex.com
 */


declare(strict_types=1);
namespace BookiFlex\Core\Service;

use BookiFlex\Core\Contract\Adjustment\AdjustmentInterface;
use BookiFlex\Core\Contract\Adjustment\AdjustmentMode;
use BookiFlex\Core\Contract\Adjustment\AdjustmentStrategy;
use BookiFlex\Core\Contract\Adjustment\AdjustmentType;
use BookiFlex\Core\Exception\InvalidArgumentException as DomainInvalidArgumentException;
use BookiFlex\Core\Exception\MoneyCalculationException;
use BookiFlex\Core\Model\Reservation\Adjustment;
use Brick\Math\Exception\NumberFormatException;
use Brick\Math\Exception\RoundingNecessaryException;
use Brick\Money\Money;
use Vaened\PriceEngine\Adjustments\Adjustments;
use Vaened\PriceEngine\Adjustments\Charge;
use Vaened\PriceEngine\Adjustments\Discount;
use Vaened\PriceEngine\Adjustments\Taxation\Exclusive;
use Vaened\PriceEngine\Adjustments\Taxation\Inclusive;
use Vaened\PriceEngine\Adjustments\Taxation\TaxCodes;
use Vaened\PriceEngine\Adjustments\Taxation\Taxes;
use Vaened\PriceEngine\Adjustments\Taxation\TaxScheme;
use Vaened\PriceEngine\Cashier;
use Vaened\PriceEngine\Cashiers\RegularCashier;
use Vaened\PriceEngine\Money\Amount;

defined( 'ABSPATH' ) || exit;

class CashierFactory
{
    /**
     * @param Money $price the Money object representing the price of the adjustable item.
     * @param Adjustment[] $adjustments list of adjustments for creating a cashier
     * @param int $quantity number of items
     * @param bool $exceptRefundable add only not refundable adjustments
     */
    public function __construct(
        private readonly Money $price,
        private readonly array $adjustments,
        private readonly int $quantity = 1,
        private readonly bool $exceptRefundable = false
    ) {
    }

    public static function make(Money $price, array $adjustments = [], int $quantity = 1, bool $exceptRefundable = false): Cashier
    {
        $cashier = new self($price, $adjustments, $quantity, $exceptRefundable);
        return $cashier->getCashier();
    }

    /**
     * Returns the total amount of the reservation item. Includes taxes, charges, and discounts.
     *
     * @return Cashier
     * @throws MoneyCalculationException
     */
    public function getCashier(): Cashier
    {
        try {
            $amount = Amount::taxable($this->price, TaxCodes::any());
        } catch (NumberFormatException|RoundingNecessaryException $e) {
            throw new MoneyCalculationException(
                sprintf(
                    'Failed to calculate cancellation penalty: %s',
                    $e->getMessage()
                ),
                0,
                $e,
                ['error' => $e->getMessage()]
            );
        }

        $taxes = Taxes::from($this->makeCashierAdjustments(AdjustmentType::Tax));
        $charges = Adjustments::from($this->makeCashierAdjustments(AdjustmentType::Charge));
        $discounts = Adjustments::from($this->makeCashierAdjustments(AdjustmentType::Discount));

        return new RegularCashier(
            $amount,
            $this->quantity,
            $taxes,
            $charges,
            $discounts,
        );
    }

    private function makeCashierAdjustments(?AdjustmentType $type = null): array
    {
        $filteredAdjustments = [];
        foreach ($this->adjustments as $adjustment) {
            if ($adjustment->isActive() && ($type === null || $adjustment->getType() === $type)) {
                if ($this->exceptRefundable && $adjustment->isRefundable()) {
                    continue;
                }
                $filteredAdjustments[] = $adjustment;
            }
        }
        return array_map(fn ($adjustment) => self::makeCashierAdjustment($adjustment), $filteredAdjustments);
    }

    /**
     * @throws DomainInvalidArgumentException
     */
    public static function makeCashierAdjustment(AdjustmentInterface $adjustment): Charge | Discount | TaxScheme
    {
        // Charge
        if ($adjustment->getType() === AdjustmentType::Charge && $adjustment->getMode() === AdjustmentMode::Percentage) {
            return Charge::proportional((int) $adjustment->getAmount())->named($adjustment->getCode());
        }

        if ($adjustment->getType() === AdjustmentType::Charge && $adjustment->getMode() === AdjustmentMode::Fixed) {
            return Charge::fixed($adjustment->getAmount())->named($adjustment->getCode());
        }

        // Discount
        if ($adjustment->getType() === AdjustmentType::Discount && $adjustment->getMode() === AdjustmentMode::Percentage) {
            return Discount::proportional((int) $adjustment->getAmount())->named($adjustment->getCode());
        }

        if ($adjustment->getType() === AdjustmentType::Discount && $adjustment->getMode() === AdjustmentMode::Fixed) {
            return Discount::fixed($adjustment->getAmount())->named($adjustment->getCode());
        }

        // Tax
        if ($adjustment->getType()        === AdjustmentType::Tax
            && $adjustment->getMode()     === AdjustmentMode::Percentage
            && $adjustment->getStrategy() === AdjustmentStrategy::Exclusive
        ) {
            return Exclusive::proportional((int) $adjustment->getAmount(), $adjustment->getCode());
        }

        if ($adjustment->getType()        === AdjustmentType::Tax
            && $adjustment->getMode()     === AdjustmentMode::Fixed
            && $adjustment->getStrategy() === AdjustmentStrategy::Exclusive
        ) {
            return Exclusive::fixed($adjustment->getAmount(), $adjustment->getCode());
        }

        if ($adjustment->getType()        === AdjustmentType::Tax
            && $adjustment->getMode()     === AdjustmentMode::Percentage
            && $adjustment->getStrategy() === AdjustmentStrategy::Inclusive
        ) {
            return Inclusive::proportional((int) $adjustment->getAmount(), $adjustment->getCode());
        }

        if ($adjustment->getType()        === AdjustmentType::Tax
            && $adjustment->getMode()     === AdjustmentMode::Fixed
            && $adjustment->getStrategy() === AdjustmentStrategy::Inclusive
        ) {
            return Inclusive::fixed($adjustment->getAmount(), $adjustment->getCode());
        }

        throw new DomainInvalidArgumentException('Invalid adjustment type');
    }
}
