<?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\Model\Reservation;

use BookiFlex\Core\Contract\Adjustment\AdjustableInterface;
use BookiFlex\Core\Contract\Adjustment\AdjustmentInterface;
use BookiFlex\Core\Contract\Adjustment\AdjustmentType;
use BookiFlex\Core\Contract\DeferrerEvent\DeferredEventSupportInterface;
use BookiFlex\Core\Contract\DeferrerEvent\Trait\HasDeferredEvents;
use BookiFlex\Core\Exception\MoneyCalculationException;
use BookiFlex\Core\Exception\RuntimeException as DomainRuntimeException;
use BookiFlex\Core\Model\CancellationPolicy\CancellationPolicy;
use BookiFlex\Core\Model\Customer\Customer;
use BookiFlex\Core\Model\Identifiable;
use BookiFlex\Core\Model\PaymentType\PaymentType;
use BookiFlex\Core\Model\Penalty\Penalty;
use BookiFlex\Core\Model\Reservation\Event\ReservationStatusWasChanged;
use BookiFlex\Core\Model\Reservation\Event\ReservationWasCreated;
use BookiFlex\Core\Model\Timestamps;
use BookiFlex\Core\Provider\LocalTimeZone;
use BookiFlex\Core\Service\CashierFactory;
use BookiFlex\Core\Util\DateTimeUtils;
use BookiFlex\Core\ValueObject\Totalizer;
use Brick\Math\Exception\MathException;
use Brick\Money\Exception\MoneyMismatchException;
use Brick\Money\Money;
use DateTimeImmutable;
use DateTimeZone;
use Random\RandomException;
use Vaened\PriceEngine\Cashier;

defined( 'ABSPATH' ) || exit;

/**
 * Represents a hotel/hostel reservation with accommodation items, extra services, and pricing adjustments.
 *
 * This class manages the complete lifecycle of a reservation including:
 * - Accommodation and extra service items
 * - Pricing calculations with taxes, discounts, and charges
 * - Status transitions from cart to completion
 * - Cancellation policies and penalties
 * - Group reservations support
 *
 * @package BookiFlex\Core\Model\Reservation
 */
class Reservation implements AdjustableInterface, DeferredEventSupportInterface
{
    use HasDeferredEvents;
    use Timestamps;
    use Identifiable;

    // ========================================================================
    // Properties
    // ========================================================================

    private ReservationStatus $status;
    private string $specialRequest = '';

    private DateTimeImmutable $checkInDate;
    private DateTimeImmutable $checkOutDate;

    private int $totalAmount = 0;
    private string $cancellationCode;

    /** @var ReservationAccommodationItem[] */
    private array $accommodationItems = [];

    /** @var ReservationExtraItem[] */
    private array $extraItems = [];

    /** @var AdjustmentInterface[] */
    private array $adjustments = [];

    /** @var Penalty[] */
    private array $penalties = [];

    private ?PaymentType $paymentType = null;
    private ?GroupReservation $group = null;
    private ?DateTimeImmutable $waitingPaymentUntil = null;

    private ?CancellationActor $cancelledBy = null;
    private ?DateTimeImmutable $cancelledAt = null;
    private ?string $cancellationReason = null;

    private ?Cashier $cachedSummary = null;

    // ========================================================================
    // Constructor
    // ========================================================================

    /**
     * Creates a new reservation instance.
     *
     * @param string $sid Internal reservation ID (system identifier)
     * @param string $number Public reservation number shown to customers
     * @param string $currency Currency code (e.g., 'USD', 'EUR')
     * @param string $checkInTime Default check-in time (HH:MM format)
     * @param string $checkOutTime Default check-out time (HH:MM format)
     * @param string $originSourceCode Source system code (e.g., 'BOOKING_COM', 'DIRECT')
     * @param ReservationAccommodationItem[] $accommodationItems Initial accommodation items
     * @param Customer $customer Customer who made the reservation
     * @param CancellationPolicy $cancellationPolicy Applied cancellation policy
     * @param string $language Language code for communications (default: 'en')
     * @param string $status Initial status (default: CART)
     *
     * @throws DomainRuntimeException If invalid data provided
     */
    public function __construct(
        private readonly string $sid,
        private readonly string $number,
        private readonly string $currency,
        private string $checkInTime,
        private string $checkOutTime,
        private readonly string $originSourceCode,
        array $accommodationItems,
        private readonly Customer $customer,
        private readonly CancellationPolicy $cancellationPolicy,
        private readonly string $language = 'en',
        string $status = ReservationStatus::STATUS_CART
    ) {
        // Initialize accommodation items
        foreach ($accommodationItems as $accommodationItem) {
            if ($accommodationItem instanceof ReservationAccommodationItem) {
                $this->addAccommodationItem($accommodationItem);
            }
        }

        // Ensure reservation lifecycle follows expected flow
        if ($status !== ReservationStatus::STATUS_CART) {
            throw new DomainRuntimeException(
                'Initial reservation status must be CART. Use markAs* methods for status transitions.'
            );
        }

        // Initialize status and timestamps
        $this->status = new ReservationStatus($status);
        $this->createdAt = DateTimeUtils::nowUtc();
        $this->cancellationCode = $this->generateCancellationCode();

        // Normalize check-in/out times to HH:MM:SS format
        [$h, $m] = explode(':', $this->checkInTime);
        $this->checkInTime = "$h:$m:00";

        [$h, $m] = explode(':', $this->checkOutTime);
        $this->checkOutTime = "$h:$m:00";

        // Dispatch creation event
        $this->deferEvent(
            new ReservationWasCreated($this->sid, [
                'id' => $this->id,
                'number' => $this->number,
                'status' => $this->status,
                'originSourceCode' => $this->originSourceCode,
            ])
        );
    }

    // ========================================================================
    // Basic Properties Access
    // ========================================================================

    /**
     * Get internal system identifier.
     */
    public function getSid(): string
    {
        return $this->sid;
    }

    /**
     * Get public reservation number.
     */
    public function getNumber(): string
    {
        return $this->number;
    }

    /**
     * Get currency code.
     */
    public function getCurrency(): string
    {
        return $this->currency;
    }

    /**
     * Get language code for communications.
     */
    public function getLanguage(): string
    {
        return $this->language;
    }

    /**
     * Get current reservation status.
     */
    public function getStatus(): ReservationStatus
    {
        return $this->status;
    }

    /**
     * Get special requests from customer.
     */
    public function getSpecialRequest(): string
    {
        return $this->specialRequest;
    }

    /**
     * Set special requests from customer.
     */
    public function setSpecialRequest(string $value): void
    {
        $this->specialRequest = $value;
    }

    // ========================================================================
    // Customer and Relations Management
    // ========================================================================

    /**
     * Get customer who made the reservation.
     */
    public function getCustomer(): ?Customer
    {
        return $this->customer;
    }

    /**
     * Get group this reservation belongs to.
     */
    public function getGroup(): ?GroupReservation
    {
        return $this->group;
    }

    /**
     * Set or remove group association.
     *
     * @param GroupReservation|null $group Group to associate with, or null to remove
     */
    public function setGroup(?GroupReservation $group = null): self
    {
        $this->group = $group;
        return $this;
    }

    /**
     * Get payment type for this reservation.
     */
    public function getPaymentType(): ?PaymentType
    {
        return $this->paymentType;
    }

    /**
     * Set payment type for this reservation.
     */
    public function setPaymentType(PaymentType $paymentType): void
    {
        $this->paymentType = $paymentType;

        if ($this->getStatus()->isEquals(ReservationStatus::STATUS_WAITING_PAYMENT) && $this->getPaymentType()?->isPayable()) {
            if ($dateTime = $this->getPaymentType()->getDateTimeForConfirmation()) {
                $this->setWaitingPaymentUntil($dateTime);
            }
        }
    }

    // ========================================================================
    // Accommodation Items Management
    // ========================================================================

    /**
     * Get all accommodation items (rooms/beds).
     *
     * @return ReservationAccommodationItem[]
     */
    public function getAccommodationItems(): array
    {
        return $this->accommodationItems;
    }

    /**
     * Add accommodation item to the reservation.
     * Updates check-in/out dates and recalculates total amount.
     *
     * @param ReservationAccommodationItem $item Item to add
     * @return $this
     */
    public function addAccommodationItem(ReservationAccommodationItem $item): self
    {
        if (in_array($item, $this->accommodationItems, true)) {
            return $this;
        }

        $this->accommodationItems[] = $item;
        $item->setReservation($this);
        $this->recalculateCheckInOutDateTime();
        $this->recalculateTotalAmount();

        return $this;
    }

    /**
     * Remove accommodation item from the reservation.
     * Updates check-in/out dates and recalculates total amount.
     *
     * @param ReservationAccommodationItem $item Item to remove
     * @return $this
     */
    public function removeAccommodationItem(ReservationAccommodationItem $item): self
    {
        foreach ($this->accommodationItems as $key => $existingItem) {
            if ($existingItem === $item) {
                unset($this->accommodationItems[$key]);
                $item->setReservation(null);
            }
        }
        $this->recalculateCheckInOutDateTime();
        $this->recalculateTotalAmount();

        return $this;
    }

    // ========================================================================
    // Extra Items Management
    // ========================================================================

    /**
     * Get all extra service items.
     *
     * @return ReservationExtraItem[]
     */
    public function getExtraItems(): array
    {
        return $this->extraItems;
    }

    /**
     * Add extra service item to the reservation.
     *
     * @param ReservationExtraItem $item Extra service to add
     * @return $this
     */
    public function addExtraItem(ReservationExtraItem $item): self
    {
        if (in_array($item, $this->extraItems, true)) {
            return $this;
        }

        $this->extraItems[] = $item;
        $item->setReservation($this);
        $this->recalculateTotalAmount();

        return $this;
    }

    /**
     * Remove extra service item from the reservation.
     *
     * @param ReservationExtraItem $item Extra service to remove
     * @return $this
     */
    public function removeExtraItem(ReservationExtraItem $item): self
    {
        foreach ($this->extraItems as $key => $existingItem) {
            if ($existingItem === $item) {
                unset($this->extraItems[$key]);
                $item->setReservation(null);
            }
        }
        $this->recalculateTotalAmount();

        return $this;
    }

    // ========================================================================
    // Adjustments Management (Taxes, Discounts, Charges)
    // ========================================================================

    /**
     * Get all pricing adjustments applied to the reservation.
     *
     * @return AdjustmentInterface[]
     */
    public function getAdjustments(): array
    {
        return $this->adjustments;
    }

    /**
     * Add pricing adjustment (tax, discount, or charge).
     *
     * @param AdjustmentInterface $adjustment Adjustment to add
     * @return $this
     */
    public function addAdjustment(AdjustmentInterface $adjustment): self
    {
        if (in_array($adjustment, $this->adjustments, true)) {
            return $this;
        }

        $this->adjustments[] = $adjustment;
        $adjustment->setAdjustable($this);
        $this->invalidateCachedSummary();
        $this->recalculateTotalAmount();

        return $this;
    }

    /**
     * Remove pricing adjustment from the reservation.
     *
     * @param AdjustmentInterface $adjustment Adjustment to remove
     * @return $this
     */
    public function removeAdjustment(AdjustmentInterface $adjustment): self
    {
        foreach ($this->adjustments as $key => $existingAdjustment) {
            if ($existingAdjustment === $adjustment) {
                unset($this->adjustments[$key]);
                $adjustment->setAdjustable(null);
                $this->invalidateCachedSummary();
                $this->recalculateTotalAmount();
            }
        }

        return $this;
    }

    // ========================================================================
    // Financial Calculations
    // ========================================================================

    /**
     * Get base total amount (sum of all items without adjustments).
     *
     * @return Money Base amount before taxes, discounts, and charges
     */
    public function getBaseTotalAmount(): Money
    {
        $baseTotalAmount = Money::zero($this->getCurrency());

        // Add accommodation items
        foreach ($this->accommodationItems as $item) {
            $baseTotalAmount = $baseTotalAmount->plus($item->getTotalPrice());
        }

        // Add non-neutral extra items
        foreach ($this->extraItems as $item) {
            if (!$item->isNeutral()) {
                $baseTotalAmount = $baseTotalAmount->plus($item->getTotalPrice());
            }
        }

        return $baseTotalAmount;
    }

    /**
     * Get final total amount including all adjustments.
     *
     * @return Money Final amount to be paid by customer
     */
    public function getTotalAmount(): Money
    {
        return Money::ofMinor($this->totalAmount, $this->getCurrency());
    }

    /**
     * Get cashier instance for detailed pricing breakdown.
     * Provides access to taxes, discounts, charges calculations.
     *
     * @return Cashier Pricing engine instance with all adjustments applied
     */
    public function getCashier(): Cashier
    {
        if ($this->cachedSummary === null) {
            $this->cachedSummary = CashierFactory::make(
                Money::zero($this->currency),
                $this->adjustments
            );
        }
        return $this->cachedSummary;
    }

    /**
     * Aggregate cashiers from all accommodation items.
     * Useful for detailed breakdown by accommodation.
     *
     * @return Cashier[]
     */
    public function aggregateAccommodationCashiers(): array
    {
        $cashiers = [];
        foreach ([...$this->accommodationItems, ...$this->extraItems] as $item) {
            $cashiers[] = $item->getCashier();
        }
        return $cashiers;
    }

    /**
     * Aggregate cashiers from extra service items.
     *
     * @param bool $ignoreNeutral Skip neutral (free) items
     * @param bool $ignoreRefundable Skip refundable items
     * @return Cashier[]
     */
    public function aggregateExtraCashiers(
        bool $ignoreNeutral = false,
        bool $ignoreRefundable = false
    ): array {
        $cashiers = [];
        foreach ($this->extraItems as $item) {
            if ($ignoreNeutral && $item->isNeutral()) {
                continue;
            }
            if ($ignoreRefundable && $item->isRefundable()) {
                continue;
            }
            $cashiers[] = $item->getCashier();
        }
        return $cashiers;
    }

    // ========================================================================
    // Date and Time Management
    // ========================================================================

    /**
     * Get check-in date.
     *
     * @return DateTimeImmutable Check-in date
     */
    public function getCheckInDate(): DateTimeImmutable
    {
        return clone $this->checkInDate;
    }

    /**
     * Get check-out date.
     *
     * @return DateTimeImmutable Check-out date
     */
    public function getCheckOutDate(): DateTimeImmutable
    {
        return clone $this->checkOutDate;
    }

    /**
     * Get minimum possible check-in datetime in local timezone.
     * Combines check-in date with property's check-in time.
     *
     * @return DateTimeImmutable|null Check-in datetime or null if invalid
     */
    public function getMinPossibleCheckInDateTime(): ?DateTimeImmutable
    {
        return DateTimeImmutable::createFromFormat(
            'Y-m-d H:i:s',
            $this->checkInDate->format('Y-m-d') . ' ' . $this->checkInTime,
            LocalTimeZone::get()
        );
    }

    /**
     * Get maximum possible check-out datetime in local timezone.
     * Combines check-out date with property's check-out time.
     *
     * @return DateTimeImmutable|null Check-out datetime or null if invalid
     */
    public function getMaxPossibleCheckOutDateTime(): ?DateTimeImmutable
    {
        return DateTimeImmutable::createFromFormat(
            'Y-m-d H:i:s',
            $this->checkOutDate->format('Y-m-d') . ' ' . $this->checkOutTime,
            LocalTimeZone::get()
        );
    }

    /**
     * Get number of nights (accommodation items count).
     *
     * @return int Number of nights in the reservation
     */
    public function getLengthOfStay(): int
    {
        return count($this->accommodationItems);
    }

    // ========================================================================
    // Payment Waiting Period Management
    // ========================================================================

    /**
     * Check if reservation is currently waiting for payment.
     *
     * @return bool True if waiting for payment and deadline not passed
     */
    public function isWaitingPayment(): bool
    {
        $now = DateTimeUtils::nowAtLocalTimeZone();

        return $this->status->isEquals(ReservationStatus::STATUS_WAITING_PAYMENT)
            && (!$this->waitingPaymentUntil || ($this->getWaitingPaymentUntil() > $now));
    }

    /**
     * Get payment deadline in local timezone.
     *
     * @return DateTimeImmutable|null Payment deadline or null if not set
     */
    public function getWaitingPaymentUntil(): ?DateTimeImmutable
    {
        return $this->waitingPaymentUntil?->setTimezone(LocalTimeZone::get());
    }

    /**
     * Set payment deadline.
     *
     * @param DateTimeImmutable $dateTime Deadline in local timezone
     */
    protected function setWaitingPaymentUntil(DateTimeImmutable $dateTime): void
    {
        $this->waitingPaymentUntil = $dateTime->setTimezone(new DateTimeZone('UTC'));
    }

    /**
     * Get who cancelled the reservation.
     */
    public function getCancelledBy(): ?CancellationActor
    {
        return $this->cancelledBy;
    }

    /**
     * Get when the reservation was cancelled.
     */
    public function getCancelledAt(): ?DateTimeImmutable
    {
        return $this->cancelledAt;
    }

    /**
     * Get the reason for cancellation.
     */
    public function getCancellationReason(): ?string
    {
        return $this->cancellationReason;
    }

    /**
     * Extend payment deadline for overdue reservations.
     *
     * @param int $hours Number of hours to extend (default: 12)
     * @throws \Exception If date modification fails
     */
    public function prolongTimeForPayment(int $hours = 12): void
    {
        $waitingPaymentUntil = DateTimeUtils::nowAtLocalTimeZone()
            ->modify("+$hours hours");

        $this->setWaitingPaymentUntil($waitingPaymentUntil);

        if ($this->status->isEquals(ReservationStatus::STATUS_OVERDUE)) {
            $this->setStatus(ReservationStatus::STATUS_WAITING_PAYMENT);
        }
    }

    /**
     * Calculate prepayment amount based on PaymentType configuration.
     *
     * @return Money Prepayment amount (zero if no payment type or on-arrival)
     * @throws MoneyCalculationException If calculation fails
     */
    public function getPrepaymentAmount(): Money
    {
        if ($this->paymentType === null) {
            return Money::zero($this->currency);
        }

        return $this->paymentType->calculatePrepayment(
            $this->getTotalAmount(),
            $this->getLengthOfStay()
        );
    }

    // ========================================================================
    // Cancellation and Penalties Management
    // ========================================================================

    /**
     * Get applied cancellation policy.
     */
    public function getCancellationPolicy(): CancellationPolicy
    {
        return $this->cancellationPolicy;
    }

    /**
     * Get unique cancellation code for customer verification.
     */
    public function getCancellationCode(): string
    {
        return $this->cancellationCode;
    }

    /**
     * Get all applied penalties.
     *
     * @return Penalty[]
     */
    public function getPenalties(): array
    {
        return $this->penalties;
    }

    /**
     * Set penalties for this reservation.
     * Replaces any existing penalties.
     *
     * @param Penalty[] $penalties Array of penalties to apply
     */
    public function addPenalties(array $penalties): void
    {
        $this->penalties = [];
        foreach ($penalties as $penalty) {
            if (!$penalty instanceof Penalty) {
                continue;
            }
            $penalty->setReservation($this);
            $this->penalties[] = $penalty;
        }
    }

    /**
     * Calculate penalties that would apply if cancelled now.
     * Uses the CancellationPolicy to calculate penalties.
     *
     * @param \DateTimeImmutable|null $cancellationDate Date of cancellation (null = now)
     * @return Penalty[] Calculated penalties based on current date and policy
     * @throws MoneyCalculationException If calculation fails
     */
    public function calculatePenalties(?\DateTimeImmutable $cancellationDate = null): array
    {
        return $this->cancellationPolicy->calculatePenalties($this, $cancellationDate);
    }

    /**
     * Get base amount for cancellation fee calculation (excepted refundable adjustments).
     *
     * @param int|null $days Calculate for first N days only (null = all days)
     * @return Money Base amount for penalty calculation
     * @throws MoneyCalculationException If calculation fails
     */
    public function getCancellationBaseSummary(?int $days = null): Money
    {
        $summaries[] = CashierFactory::make(
            Money::zero($this->currency),
            $this->filterChargesForPenalty($this->adjustments),
            1,
            true
        );

        $count = 0;
        foreach ($this->accommodationItems as $item) {
            $summaries[] = CashierFactory::make(
                $item->getBasePrice(),
                $this->filterChargesForPenalty($item->getAdjustments()),
                1,
                true
            );
            $count++;

            if ($days !== null && $count >= $days) {
                break;
            }
        }

        // Include non-refundable extra items
        foreach ($this->extraItems as $item) {
            if (!$item->isRefundable() && !$item->isNeutral()) {
                $summaries[] = CashierFactory::make(
                    $item->getBasePrice(),
                    $this->filterChargesForPenalty($item->getAdjustments()),
                    1,
                    true
                );
            }
        }

        return Totalizer::of($summaries, $this->currency)->summary()->total();
    }

    /**
     * Drop refundable Charge adjustments from penalty base — they are returned to the guest on cancellation.
     * Tax and Discount adjustments are kept as-is.
     *
     * @param iterable<AdjustmentInterface> $adjustments
     * @return array<AdjustmentInterface>
     */
    private function filterChargesForPenalty(iterable $adjustments): array
    {
        $kept = [];
        foreach ($adjustments as $adjustment) {
            if ($adjustment->getType() === AdjustmentType::Charge && $adjustment->isRefundable()) {
                continue;
            }
            $kept[] = $adjustment;
        }
        return $kept;
    }

    // ========================================================================
    // Status Transitions
    // ========================================================================

    /**
     * Mark reservation as waiting for confirmation from property.
     */
    public function markAsWaitingForConfirmation(): void
    {
        $this->setStatus(ReservationStatus::STATUS_WAITING_CONFIRMATION);
    }

    /**
     * Mark reservation as waiting for payment.
     * Sets payment deadline based on payment type configuration.
     */
    public function markAsWaitingForPayment(): void
    {
        try {
            if ($this->getPaymentType()?->isPayable()) {
                if ($dateTime = $this->getPaymentType()->getDateTimeForConfirmation()) {
                    $this->setWaitingPaymentUntil($dateTime);
                }
            }
        } catch (\Exception $e) {
            // Ignore date parsing errors
        }

        $this->setStatus(ReservationStatus::STATUS_WAITING_PAYMENT);
    }

    /**
     * Mark reservation as confirmed.
     * Transitions through waiting confirmation if coming from payment waiting.
     */
    public function markAsConfirmed(): void
    {
        if ($this->status->isEquals(ReservationStatus::STATUS_WAITING_PAYMENT)) {
            $this->setStatus(ReservationStatus::STATUS_WAITING_CONFIRMATION);
        }
        $this->setStatus(ReservationStatus::STATUS_CONFIRMED);
    }

    /**
     * Mark reservation as cancelled.
     * Calculates and applies cancellation penalties.
     *
     * @throws MoneyCalculationException If penalty calculation fails
     */
    public function markAsCancelled(CancellationActor $actor = CancellationActor::Admin, bool $applyPenalty = true, ?string $reason = null): void
    {
        $this->cancelledBy = $actor;
        $this->cancelledAt = DateTimeUtils::nowUtc();
        $this->cancellationReason = $reason;

        $currentStatus = $this->status->getValue();
        $this->setStatus(ReservationStatus::STATUS_CANCELLED);

        if ($applyPenalty
            && ($currentStatus === ReservationStatus::STATUS_CONFIRMED)
        ) {
            $this->addPenalties($this->calculatePenalties());
        }
    }

    /**
     * Mark reservation as overdue (payment not received in time).
     * Only applicable for reservations waiting for payment.
     */
    public function markAsOverdue(): void
    {
        if ($this->status->isEquals(ReservationStatus::STATUS_WAITING_PAYMENT)) {
            $this->setStatus(ReservationStatus::STATUS_OVERDUE);
        }
    }

    /**
     * Mark reservation as in-house (guest has checked in).
     * Only applicable for confirmed reservations on or after check-in date.
     */
    public function markAsInHouse(): void
    {
        $today = DateTimeUtils::nowAtLocalTimeZone()->format('Y-m-d');

        if ($this->status->isEquals(ReservationStatus::STATUS_CONFIRMED)
            && $today >= $this->checkInDate->format('Y-m-d')
        ) {
            $this->setStatus(ReservationStatus::STATUS_IN_HOUSE);
        }
    }

    /**
     * Mark reservation as completed (guest has checked out).
     * Only applicable for in-house reservations.
     */
    public function markAsCompleted(): void
    {
        if ($this->status->isEquals(ReservationStatus::STATUS_IN_HOUSE)) {
            $this->setStatus(ReservationStatus::STATUS_COMPLETED);
        }
    }

    /**
     * Mark reservation as no-show (guest didn't arrive).
     * Only applicable for confirmed reservations on or after check-in date.
     * @throws MoneyCalculationException
     */
    public function markAsNoShow(bool $applyPenalty = true): void
    {
        $today = DateTimeUtils::nowAtLocalTimeZone()->format('Y-m-d');

        if ($this->status->isEquals(ReservationStatus::STATUS_CONFIRMED)
            && $today >= $this->checkInDate->format('Y-m-d')
        ) {
            $this->setStatus(ReservationStatus::STATUS_NO_SHOW);

            if ($applyPenalty) {
                $this->addPenalties($this->calculatePenalties());
            }
        }
    }

    // ========================================================================
    // Private Helper Methods
    // ========================================================================

    /**
     * Set reservation status with validation and event dispatching.
     *
     * @param string $status New status to transition to
     */
    private function setStatus(string $status): void
    {
        $fromStatus = (string) $this->status;

        try {
            $this->status->transitionTo($status);
            $this->group?->recalculateStatus();

            $eventPayload = ['id' => $this->id];
            if ($status === ReservationStatus::STATUS_CANCELLED && $this->cancelledBy !== null) {
                $eventPayload['cancelledBy'] = $this->cancelledBy->value;
            }

            $this->deferEvent(
                new ReservationStatusWasChanged($this->sid, $fromStatus, $status, $eventPayload)
            );
        } catch (DomainRuntimeException $e) {
            // Silently ignore invalid transitions
        }
    }

    /**
     * Recalculate total amount including all adjustments.
     *
     * @throws MathException|MoneyMismatchException If calculation fails
     */
    private function recalculateTotalAmount(): void
    {
        $amount = $this->getBaseTotalAmount()->plus($this->getCashier()->total());
        $this->totalAmount = $amount
            ->getMinorAmount()
            ->toInt();
    }

    /**
     * Recalculate check-in and check-out dates based on accommodation items.
     *
     * @throws \Exception If date modification fails
     */
    private function recalculateCheckInOutDateTime(): void
    {
        if (empty($this->accommodationItems)) {
            return;
        }

        $dates = array_map(
            fn(ReservationAccommodationItem $item) => $item->getDate(),
            $this->accommodationItems
        );

        /** @var DateTimeImmutable|false $checkInDate */
        $checkInDate = min($dates);
        if ($checkInDate) {
            $this->checkInDate = $checkInDate;
        }

        /** @var DateTimeImmutable|false $checkOutDate */
        $checkOutDate = max($dates);
        if ($checkOutDate) {
            $this->checkOutDate = $checkOutDate->modify('+1 day');
        }
    }

    /**
     * Generate random 5-digit cancellation code.
     *
     * @return string Cancellation code
     */
    private function generateCancellationCode(): string
    {
		try {
			$rand = random_int(10000, 99999);
		} catch ( RandomException $e ) {
			$rand = mt_rand(10000, 99999);
		}

	    return (string) $rand;
    }

    /**
     * Invalidate cached pricing summary.
     * Called when adjustments are modified.
     */
    private function invalidateCachedSummary(): void
    {
        $this->cachedSummary = null;
    }
}
