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

declare(strict_types=1);

 namespace BookiFlex\Extensions\Payment\Service;

use BookiFlex\Core\Contract\Payment\PaymentFactoryInterface;
use BookiFlex\Core\Exception\MoneyCalculationException;
use BookiFlex\Extensions\Payment\Model\Payment;
use BookiFlex\Extensions\Payment\Model\PaymentAllocation;
use BookiFlex\Extensions\Payment\Model\PaymentAllocationType;
use BookiFlex\Core\Model\Reservation\Reservation;
use BookiFlex\Core\Model\Reservation\ReservationStatus;
use BookiFlex\Core\Util\DateTimeUtils;
use Brick\Math\Exception\MathException;
use Brick\Money\Exception\MoneyMismatchException;
use Brick\Money\Exception\UnknownCurrencyException;
use Brick\Money\Money;

defined( 'ABSPATH' ) || exit;

/**
 * Creates a payment for a set of reservations and allocates it to them.
 * Payment will be grouped by payment type.
 * And changes reservation statuses.
 */
class PaymentFactory implements PaymentFactoryInterface
{
    /**
     * @param Reservation[] $reservations
     * @return Payment[]
     * @throws MoneyCalculationException
     */
    public function createPayments(array $reservations): array
    {
        if (empty($reservations)) {
            return [];
        }

        // Get only new reservations
        $reservations = array_values(array_filter(
            $reservations,
            fn (Reservation $reservation) => $reservation->getStatus()->isEquals(ReservationStatus::STATUS_WAITING_PAYMENT)
        ));

        // Create payment and allocate it to reservations if payment type is payable and has payment system.
        try {
            return $this->createAndAllocatePayment($reservations);
        } catch (MathException|MoneyMismatchException|UnknownCurrencyException $e) {
            // TODO: log error
            return [];
        }
    }

    /**
     * @param Reservation[] $reservations
     * @return Payment[]
     *
     * @throws MathException
     * @throws MoneyMismatchException
     * @throws UnknownCurrencyException
     * @throws \Brick\Money\Exception\MoneyMismatchException
     * @throws MoneyCalculationException
     */
    private function createAndAllocatePayment(array $reservations): array
    {
        $prepaymentAmounts = [];
        foreach ($reservations as $reservation) {
            $paymentType = $reservation->getPaymentType();
            if ($paymentType === null || !$paymentType->isPayable()) {
                continue;
            }

            if (empty($paymentType->getGatewayName())) {
                continue;
            }

            $prepaymentAmounts[$paymentType->getId()][] = [
                'reservation' => $reservation,
                'prepayment'  => $paymentType->calculatePrepayment(
                    $reservation->getTotalAmount(),
                    $this->getMaxLengthOfStay($reservation)
                ),
            ];
        }

        /**
         * Create independent payments for different payment types.
         * If few reservations have the same payment type, create one payment for them.
         */
        $payments = [];
        foreach ($prepaymentAmounts as $prepaymentAmount) {
            $payment = new Payment();
            $payment->setClient($prepaymentAmount[0]['reservation']->getCustomer());
            $payment->setGatewayName($prepaymentAmount[0]['reservation']->getPaymentType()->getGatewayName());
            $payment->setNumber(uniqid());
            $payment->setAmount(Money::total(...array_column($prepaymentAmount, 'prepayment')));
            $payment->setDescription('#' . $prepaymentAmount[0]['reservation']->getNumber());

            foreach ($prepaymentAmount as $prepayment) {
                $reservation      = $prepayment['reservation'];
                $allocatedPayment = new PaymentAllocation(
                    PaymentAllocationType::RESERVATION,
                    $payment,
                    $reservation,
                    $prepayment['prepayment']
                );

                $payment->allocatePayment($allocatedPayment);
            }

            $payments[] = $payment;
        }

        return $payments;
    }

    /**
     * @param Reservation $reservation
     * @return int
     */
    private function getMaxLengthOfStay(Reservation $reservation): int
    {
        try {
            return DateTimeUtils::lengthOfStay($reservation->getMinPossibleCheckInDateTime(), $reservation->getMaxPossibleCheckOutDateTime());
        } catch (\Throwable $e) {
            return 0;
        }
    }
}
