<?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\Contract\AvailabilityChecker\QuotaAvailabilityInterface;
use BookiFlex\Core\Contract\Fee\FeeCalculatorInterface;
use BookiFlex\Core\Contract\Payment\PaymentFactoryInterface;
use BookiFlex\Core\Contract\Taxation\TaxCalculatorInterface;
use BookiFlex\Extensions\Cart\Contract\CartExtraProviderInterface;
use BookiFlex\Extensions\Cart\Model\AccommodationCartItem;
use BookiFlex\Extensions\Cart\Model\Cart;
use BookiFlex\Extensions\Cart\Model\ExtraCartItem;
use BookiFlex\Extensions\Cart\Service\DTO\CheckoutResult;
use BookiFlex\Core\Model\Reservation\Reservation;
use BookiFlex\Core\Model\Reservation\ReservationAccommodationItem;
use BookiFlex\Core\Model\Reservation\ReservationExtraItem;
use BookiFlex\Core\Repository\AccommodationTypeRepository;
use BookiFlex\Core\Repository\RatePlanRepository;
use BookiFlex\Core\Service\ConfigurationService;
use BookiFlex\Core\Service\NumberGenerator\NumberGenerator;
use BookiFlex\Core\Service\StayCostCalculator\StayCostCalculator;
use Brick\Money\Money;
use Cycle\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
use RuntimeException;
use Symfony\Component\Uid\Uuid;

defined( 'ABSPATH' ) || exit;

/**
 * Service for converting cart to reservations
 */
class CartCheckoutService
{
    public function __construct(
        private readonly RatePlanRepository $ratePlanRepository,
        private readonly AccommodationTypeRepository $accommodationTypeRepository,
        private readonly NumberGenerator $reservationNumberGenerator,
        private readonly ConfigurationService $configurationService,
        private readonly StayCostCalculator $stayCostCalculator,
        private readonly EntityManagerInterface $entityManager,
        private readonly ?QuotaAvailabilityInterface $quotaAvailability = null,
        private readonly ?PaymentFactoryInterface $paymentFactory = null,
        private readonly ?TaxCalculatorInterface $taxCalculator = null,
        private readonly ?FeeCalculatorInterface $feeCalculator = null,
	    private readonly ?LoggerInterface $logger = null,
        private readonly ?CartExtraProviderInterface $extraProvider = null,
    ) {
    }

    /**
     * Process checkout and create reservations
     *
     * @return CheckoutResult
     */
    public function checkout(Cart $cart): CheckoutResult
    {
        // Guard against a cart whose currency went stale (site currency changed while
        // it was open). Its items are priced in the old currency and cannot be turned
        // into a reservation, so reset the cart to the current currency and abort.
        // In the normal HTTP flow CartService::getCart() has already reset it; this
        // protects the public checkout() entry point regardless of how the cart arrived.
        $currentCurrency = $this->configurationService->getDefaultCurrency();
        if ($cart->getCurrency() !== $currentCurrency) {
            $cart->resetCurrency($currentCurrency);
            $this->entityManager->persist($cart);
            $this->entityManager->run();

            return CheckoutResult::failure([
                'currency_changed' => __(
                    'The site currency changed, so your cart was reset. Please add your items again.',
                    'bookiflex'
                ),
            ]);
        }

        // Validate cart before processing
        $validationErrors = $this->validateCart($cart);
        if (!empty($validationErrors)) {
            return CheckoutResult::failure($validationErrors);
        }

        try {
            $this->reservationNumberGenerator->reset();

            $reservations = [];
            $reservationsByAccommodationHash = [];

            // Process accommodation items
            $accommodationItems = $cart->getItemsByType('accommodation');
            foreach ($accommodationItems as $cartItem) {
                $domainItem = $cartItem->toDomainObject();
                if ($domainItem instanceof AccommodationCartItem) {
                    $itemReservations = $this->createAccommodationReservations(
                        $domainItem,
                        $cart,
                        $cartItem->getQuantity(),
                        $cartItem->getUnitPrice()
                    );
                    $reservations = array_merge($reservations, $itemReservations);
                    $reservationsByAccommodationHash[$domainItem->getHash()] = $itemReservations;
                }
            }

            // Attach extras (self-selected and included) to their parent reservations.
            $this->attachExtrasToReservations($cart, $reservationsByAccommodationHash);

            // Create group reservation if multiple items
            if (count($reservations) > 1) {
                $this->createGroupReservation($reservations);
            }

            // Update reservation statuses based on payment types
            $this->updateReservationStatuses($reservations);

            // Create payments if factory available
            if ($this->paymentFactory) {
                $payments = $this->paymentFactory->createPayments($reservations);

                foreach ($payments as $payment) {
                    $this->entityManager->persist($payment);
                }
            }

            // Save all reservations
            foreach ($reservations as $reservation) {
                $this->entityManager->persist($reservation);
            }

            $this->entityManager->run();

            // Mark cart as completed
            $cart->complete();
            $this->entityManager->persist($cart);
            $this->entityManager->run();

            return CheckoutResult::success($reservations);
        } catch (\Throwable $e) {
            $errorMessage = sprintf(
                'Failed to save reservations: %s',
                $e->getMessage()
            );
            return CheckoutResult::failure([$errorMessage]);
        }
    }

    /**
     * Validate cart before checkout
     *
     * @return array Array of error messages (empty if valid)
     */
    private function validateCart(Cart $cart): array
    {
        $errors = [];

        // Basic validation
        if ($cart->isEmpty()) {
            $errors['cart_empty'] = __('Cart is empty', 'bookiflex');
        }

        if (!$cart->getCustomer()) {
            $errors['customer_required'] = __('Customer information is required', 'bookiflex');
        }

        // Cart-level validation (items, business rules)
        $cartErrors = $cart->validate();
        if (!empty($cartErrors)) {
            $errors = array_merge($errors, $cartErrors);
        }

        // Quota validation
        foreach ($cart->getItemsByType('accommodation') as $cartItem) {
            $item = $cartItem->toDomainObject();
            if ($item instanceof AccommodationCartItem) {
                $quota = $this->quotaAvailability?->getQuota(
                    $item->getStayPeriod(),
                    $item->getAccommodationSelection()->getAccommodationTypeId()
                ) || 100;

                if ($quota < $cartItem->getQuantity()) {
                    $errors["insufficient_quota_{$cartItem->getHash()}"] = sprintf(
                        /* translators: %s: accommodation name */
                        __('Not enough availability for %s', 'bookiflex'),
                        $item->getName()
                    );
                }
            }
        }

        return $errors;
    }

    /**
     * Create reservations from accommodation cart item
     */
    private function createAccommodationReservations(
        AccommodationCartItem $item,
        Cart $cart,
        int $quantity,
        int $unitPrice
    ): array {
        $reservations = [];

        // Get entities
        $ratePlan = $this->ratePlanRepository->findByPK(
            $item->getAccommodationSelection()->getRatePlanId()
        );
        $accommodationType = $this->accommodationTypeRepository->findByPK(
            $item->getAccommodationSelection()->getAccommodationTypeId()
        );

        if (!$ratePlan || !$accommodationType) {
            throw new RuntimeException('Rate plan or accommodation type not found');
        }

        $stayCostResult = $this->stayCostCalculator->calculate(
            $item->getStayPeriod(),
            $item->getAccommodationSelection(),
            $item->getGuestConfiguration()
        );

        $taxableObjects = [$stayCostResult];
        foreach ($stayCostResult->getDailyCosts() as $dailyCost) {
            $taxableObjects[] = $dailyCost;
        }

        $this->taxCalculator?->applyTaxes($taxableObjects);
        $this->feeCalculator?->applyFees($taxableObjects);

        // If price was changed
        if (!$stayCostResult->getFinalPrice()->getMinorAmount()->isEqualTo($unitPrice)) {
            /* translators: %1$s: old price, %2$s: new price, %3$s: currency */
            $errorMessage = __('Price is changed from %1$s to %2$s %3$s', 'bookiflex');
            throw new RuntimeException(sprintf($errorMessage, $unitPrice, $stayCostResult->getFinalPrice()->getMinorAmount()->toInt(), $cart->getCurrency()));
        }

        $item->setStayCostResult($stayCostResult);

        // Prepare payment type
        $paymentType = null;
        foreach ($ratePlan->getPaymentTypes() as $pt) {
            if ($pt->getId() === $item->getPaymentTypeId()) {
                $paymentType = $pt;
                break;
            }
        }

        // Prepare special request
        $specialRequest = $cart->getSpecialRequest() ?? '';
        if ($cart->getArrivalTime()) {
            $arrivalNote = sprintf(
                /* translators: %s: arrival time */
				__('Estimated arrival time: %s;', 'bookiflex'),
				$cart->getArrivalTime()
            );
            $specialRequest = $arrivalNote . ($specialRequest ? "\n" . $specialRequest : '');
        }

        // Create reservation for each quantity
        for ($i = 0; $i < $quantity; $i++) {
            $reservation = new Reservation(
                (string) Uuid::v4(),
                $this->reservationNumberGenerator->nextNumber(),
                $cart->getCurrency(),
                $cart->getCheckInTime(),
                $cart->getCheckOutTime(),
                'web', // origin source
                [],
                $cart->getCustomer(),
                $ratePlan->getCancellationPolicy(),
                $this->configurationService->getCurrentLanguage()
            );

            // Set special request and arrival time
            $reservation->setSpecialRequest($specialRequest);

            // Set payment type
            if ($paymentType) {
                $reservation->setPaymentType($paymentType);
            }

            // Add accommodation items for each day
            if ($stayCostResult = $item->getStayCostResult()) {
                foreach ($stayCostResult->getDailyCosts() as $dailyCost) {
                    $accommodationItem = new ReservationAccommodationItem(
                        $accommodationType->getName() . ' (' . $ratePlan->getName() . ')',
                        $dailyCost->getBasePrice(),
                        $dailyCost->getDate(),
                        $accommodationType->getShortCode(),
                        $ratePlan->getCode(),
                        $item->getGuestConfiguration()->getAdults(),
                        $item->getGuestConfiguration()->getChildren(),
                        $dailyCost->getPriceDetails()
                    );

                    // Add adjustments if any
                    foreach ($dailyCost->getAdjustments() as $adjustment) {
                        $accommodationItem->addAdjustment(clone $adjustment);
                    }

                    $reservation->addAccommodationItem($accommodationItem);
                }

                // Add reservation-level adjustments
                foreach ($stayCostResult->getBookingAdjustments() as $adjustment) {
                    $reservation->addAdjustment(clone $adjustment);
                }
            }

            $reservations[] = $reservation;
        }

        return $reservations;
    }

    /**
     * Attach cart extra items (self-selected and included) to the matching parent
     * reservations. Each accommodation reservation gets its own clone of the extra
     * (for accommodation cart items with quantity > 1, the extra is copied per
     * reservation). Included extras are forwarded as neutral so they don't add
     * to the reservation total.
     *
     * @param array<string, Reservation[]> $reservationsByAccommodationHash
     */
    private function attachExtrasToReservations(Cart $cart, array $reservationsByAccommodationHash): void
    {
        $extraItems = $cart->getItemsByType(ExtraCartItem::TYPE);
        if (empty($extraItems)) {
            return;
        }

        foreach ($extraItems as $cartItem) {
            $extra = $cartItem->toDomainObject();
            if (!$extra instanceof ExtraCartItem) {
                continue;
            }

            $parentReservations = $reservationsByAccommodationHash[$extra->getParentHash()] ?? [];
            foreach ($parentReservations as $reservation) {
                $reservation->addExtraItem($this->buildReservationExtraItem($extra, $cart->getCurrency()));
            }
        }
    }

    private function buildReservationExtraItem(ExtraCartItem $cartItem, string $currency): ReservationExtraItem
    {
        $unitPrice = Money::ofMinor($cartItem->getUnitPrice() ?? 0, $currency);

        $item = new ReservationExtraItem(
            name: $cartItem->getName(),
            price: $unitPrice,
            quantity: $cartItem->getQuantity(),
            code: (string) $cartItem->getExtraId(),
        );

        $snapshot = $this->extraProvider?->findExtra($cartItem->getExtraId());

        $item->setDetails([
            'extra_service_id' => $cartItem->getExtraId(),
            'included'         => $cartItem->isIncluded(),
            'service_snapshot' => $snapshot ? [
                'name'          => $snapshot->name,
                'type'          => $snapshot->type,
                'charging_rate' => $snapshot->chargingRate,
                'catalog_price' => $snapshot->catalogPrice,
                'max_per_unit'  => $snapshot->maxPerUnit,
            ] : [],
        ]);

        if ($cartItem->isIncluded()) {
            $item->setNeutral(true);
        }

        return $item;
    }

    /**
     * Create group reservation for multiple reservations
     */
    protected function createGroupReservation(array $reservations): void
    {
        // Implementation for group reservations
        // This would create a GroupReservation entity and link all reservations to it
    }

    /**
     * Update reservation statuses based on payment types
     */
    private function updateReservationStatuses(array $reservations): void
    {
        $autoConfirm = $this->configurationService->autoConfirmationForNonPayablePaymentTypes();

        foreach ($reservations as $reservation) {
            $paymentType = $reservation->getPaymentType();

            if (!$paymentType || !$paymentType->isPayable()) {
                // Non-payable reservations
                $reservation->markAsWaitingForConfirmation();
                if ($autoConfirm) {
                    $reservation->markAsConfirmed();
                }
            } else {
                // Payable reservations
                $reservation->markAsWaitingForPayment();
            }
        }
    }
}
