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


declare(strict_types=1);
namespace BookiFlex\Extensions\Cart\Service;

use BookiFlex\Core\Exception\MoneyCalculationException;
use BookiFlex\Extensions\Cart\Model\AccommodationCartItem;
use BookiFlex\Extensions\Cart\Model\Cart;
use BookiFlex\Extensions\Cart\Model\CartItem;
use BookiFlex\Extensions\Cart\Model\ExtraCartItem;
use BookiFlex\Core\Model\PaymentType\PaymentType;
use BookiFlex\Core\Model\RatePlan\RatePlan;
use BookiFlex\Core\Repository\RatePlanRepository;
use Brick\Math\Exception\MathException;
use Brick\Money\Exception\MoneyMismatchException;
use Brick\Money\Money;

defined( 'ABSPATH' ) || exit;

/**
 * Manages payment types and prepayment calculations for bookings
 */
class PaymentTypeService
{
    public function __construct(
        private readonly RatePlanRepository $ratePlanRepository,
    ) {
    }

    /**
     * Gets available payment types for cart items
     *
     * @param Cart $cart
     * @return PaymentType[][] Array [cartItemHash => PaymentType[]]
     */
    public function getAvailablePaymentTypes(Cart $cart): array
    {
        $result = [];
        $checkInDateTime = $this->createCheckInDateTime($cart);

        foreach ($cart->getDomainItems() as $item) {
            if (!$item instanceof AccommodationCartItem) {
                continue;
            }

            $ratePlan = $this->getRatePlanForItem($item);
            if ($ratePlan) {
                $result[$item->getHash()] = $ratePlan->getAvailablePaymentTypes($checkInDateTime);
            }
        }

        return $result;
    }

    /**
     * Calculates the total prepayment amount for the cart
     *
     * @throws MoneyCalculationException
     */
    public function calculateTotalPrepayment(Cart $cart): Money
    {
        $paymentTypesMap = $this->getAvailablePaymentTypes($cart);

        try {
            $totalPrepaymentAmount = Money::zero($cart->getCurrency());

            foreach ($cart->getItems() as $item) {
                $prepaymentAmount = $this->calculateItemPrepayment(
                    $item,
                    $cart,
                    $paymentTypesMap,
                    $cart->getCurrency()
                );

                if ($prepaymentAmount) {
                    $totalPrepaymentAmount = $totalPrepaymentAmount->plus($prepaymentAmount);
                }
            }

            return $totalPrepaymentAmount;

        } catch (MoneyMismatchException | MathException $e) {
            throw new MoneyCalculationException(
                sprintf(
                    'Failed to calculate prepayment: %s',
                    $e->getMessage()
                ),
                0,
                $e,
                ['errorCode' => 'money_calculation_failed', 'errors' => $e->getMessage()]
            );
        }
    }

    /**
     * Calculates prepayment for a specific cart item.
     *
     * Accommodation items use their own payment type and stay period. Extra items
     * inherit both from the parent accommodation they are attached to — this mirrors
     * how PaymentFactory allocates payments at confirmation time, so the cart and
     * the confirmed reservation agree on the prepayment / on-arrival split.
     *
     * @param array<string, PaymentType[]> $paymentTypesMap
     */
    private function calculateItemPrepayment(
        CartItem $item,
        Cart $cart,
        array $paymentTypesMap,
        string $currency
    ): ?Money {
        $domainItem = $item->toDomainObject();

        if ($domainItem instanceof AccommodationCartItem) {
            return $this->prepaymentForAccommodation($item, $domainItem, $paymentTypesMap, $currency);
        }

        if ($domainItem instanceof ExtraCartItem) {
            return $this->prepaymentForExtra($item, $domainItem, $cart, $paymentTypesMap, $currency);
        }

        return null;
    }

    /**
     * @param array<string, PaymentType[]> $paymentTypesMap
     */
    private function prepaymentForAccommodation(
        CartItem $item,
        AccommodationCartItem $domainItem,
        array $paymentTypesMap,
        string $currency
    ): ?Money {
        $paymentType = $this->resolvePaymentType(
            $paymentTypesMap[$item->getHash()] ?? [],
            $domainItem->getPaymentTypeId()
        );
        if (!$paymentType) {
            return null;
        }

        return $this->applyPrepayment(
            $paymentType,
            $item->getUnitPrice() ?? 0,
            $item->getQuantity(),
            $domainItem->getStayPeriod()->getLengthOfStay(),
            $currency
        );
    }

    /**
     * @param array<string, PaymentType[]> $paymentTypesMap
     */
    private function prepaymentForExtra(
        CartItem $item,
        ExtraCartItem $domainItem,
        Cart $cart,
        array $paymentTypesMap,
        string $currency
    ): ?Money {
        $parentItem = $this->findParentItem($cart, $domainItem->getParentHash());
        if (!$parentItem) {
            return null;
        }

        $parentDomain = $parentItem->toDomainObject();
        if (!$parentDomain instanceof AccommodationCartItem) {
            return null;
        }

        $paymentType = $this->resolvePaymentType(
            $paymentTypesMap[$parentItem->getHash()] ?? [],
            $parentDomain->getPaymentTypeId()
        );
        if (!$paymentType) {
            return null;
        }

        return $this->applyPrepayment(
            $paymentType,
            $item->getUnitPrice() ?? 0,
            $item->getQuantity(),
            $parentDomain->getStayPeriod()->getLengthOfStay(),
            $currency
        );
    }

    /**
     * @param PaymentType[] $paymentTypes
     */
    private function resolvePaymentType(array $paymentTypes, int $paymentTypeId): ?PaymentType
    {
        if (empty($paymentTypes)) {
            return null;
        }

        return $this->findPaymentTypeById($paymentTypes, $paymentTypeId);
    }

    private function applyPrepayment(
        PaymentType $paymentType,
        int $totalPrice,
        int $quantity,
        int $lengthOfStay,
        string $currency
    ): ?Money {
        try {
            $totalAmount = Money::ofMinor($totalPrice, $currency);
            $prepaymentAmount = $paymentType->calculatePrepayment($totalAmount, $lengthOfStay);
            return $prepaymentAmount->multipliedBy($quantity);
        } catch (\Exception $e) {
            return null;
        }
    }

    private function findParentItem(Cart $cart, string $parentHash): ?CartItem
    {
        foreach ($cart->getItems() as $item) {
            if ($item->getHash() === $parentHash) {
                return $item;
            }
        }

        return null;
    }

    /**
     * Creates a DateTime object for check-in time
     */
    private function createCheckInDateTime(Cart $cart): \DateTimeImmutable
    {
        [$hours, $minutes] = explode(':', $cart->getCheckInTime());

        // Assuming the first cart item contains the check-in date
        // This can be improved by passing the date explicitly
        $items = $cart->getDomainItems();
        $firstAccommodationItem = null;

        foreach ($items as $item) {
            if ($item instanceof AccommodationCartItem) {
                $firstAccommodationItem = $item;
                break;
            }
        }

        if (!$firstAccommodationItem) {
            return new \DateTimeImmutable();
        }

        return $firstAccommodationItem
            ->getStayPeriod()
            ->getCheckInDate()
            ->setTime((int) $hours, (int) $minutes);
    }

    /**
     * Gets the RatePlan for a cart item
     */
    private function getRatePlanForItem(AccommodationCartItem $item): ?RatePlan
    {
        $ratePlanId = $item->getAccommodationSelection()->getRatePlanId();
        return $this->ratePlanRepository->findByPK($ratePlanId);
    }

    /**
     * Finds payment type by ID
     */
    private function findPaymentTypeById(array $paymentTypes, int $paymentTypeId): ?PaymentType
    {
        foreach ($paymentTypes as $paymentType) {
            if ($paymentType->getId() === $paymentTypeId) {
                return $paymentType;
            }
        }

        return null;
    }
}
