<?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\Exception\MoneyCalculationException;
use BookiFlex\Core\Model\Identifiable;
use BookiFlex\Core\Model\Timestamps;
use BookiFlex\Core\Service\CashierFactory;
use BookiFlex\Core\Util\DateTimeUtils;
use Brick\Math\Exception\MathException;
use Brick\Math\Exception\NumberFormatException;
use Brick\Math\Exception\RoundingNecessaryException;
use Brick\Money\Exception\UnknownCurrencyException;
use Brick\Money\Money;
use DateTimeImmutable;
use Vaened\PriceEngine\Cashier;

defined( 'ABSPATH' ) || exit;

/**
 * Abstract base class for reservation items (accommodation and extra services).
 *
 * Provides common functionality for:
 * - Price calculations with adjustments (taxes, discounts, charges)
 * - Currency handling
 * - Association with reservation
 * - Caching of pricing summaries
 *
 * @package BookiFlex\Core\Model\Reservation
 */
abstract class AbstractReservationItem implements AdjustableInterface
{
    use Timestamps;
    use Identifiable;

    /** @var int Base price in minor currency units (e.g., cents) */
    protected int $basePrice;

    /** @var int Total price including adjustments in minor currency units */
    protected int $totalPrice;

    /** @var string Currency code (e.g., 'USD', 'EUR') */
    protected string $currency;

    /** @var Reservation|null Parent reservation */
    protected ?Reservation $reservation = null;

    /** @var AdjustmentInterface[] Applied pricing adjustments */
    protected array $adjustments = [];

    /** @var Cashier|null Cached pricing summary for performance */
    protected ?Cashier $cachedSummary = null;

    /** @var array Additional details for the item */
    protected array $details = [];

    /**
     * Constructor for reservation items.
     *
     * @param string $name Item name/description
     * @param Money $price Base price before adjustments
     *
     * @throws MoneyCalculationException If price calculation fails
     */
    public function __construct(
        protected readonly string $name,
        Money $price
    ) {
        $this->currency = $price->getCurrency()->getCurrencyCode();

        try {
            $this->basePrice = $price->getMinorAmount()->toInt();
            $this->totalPrice = $this->basePrice;
        } catch (MathException $e) {
            throw new MoneyCalculationException(
                sprintf(
                    'Failed to calculate money: %s',
                    $e->getMessage()
                ),
                0,
                $e,
                ['error' => $e->getMessage()]
            );
        }

        $this->createdAt = DateTimeUtils::nowUtc();
    }

    // ========================================================================
    // Abstract Methods
    // ========================================================================

    /**
     * Get item's date (accommodation date or service date).
     *
     * @return DateTimeImmutable|null
     */
    abstract public function getDate(): ?DateTimeImmutable;

    // ========================================================================
    // Common Properties Access
    // ========================================================================

    /**
     * Get parent reservation.
     *
     * @return Reservation|null
     */
    public function getReservation(): ?Reservation
    {
        return $this->reservation;
    }

    /**
     * Set parent reservation.
     *
     * @param Reservation|null $reservation
     * @return $this
     */
    public function setReservation(?Reservation $reservation): self
    {
        $this->reservation = $reservation;
        return $this;
    }

    /**
     * Get item name/description.
     *
     * @return string
     */
    public function getName(): string
    {
        return $this->name;
    }

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

    /**
     * Get additional details.
     *
     * @return array
     */
    public function getDetails(): array
    {
        return $this->details;
    }

    public function setDetails(array $details): self
    {
        $this->details = $details;
        return $this;
    }

    // ========================================================================
    // Pricing Methods
    // ========================================================================

    /**
     * Get base price before any adjustments.
     *
     * @return Money Base price
     * @throws MoneyCalculationException If price creation fails
     */
    public function getBasePrice(): Money
    {
        try {
            return Money::ofMinor($this->basePrice, $this->currency);
        } catch (NumberFormatException|UnknownCurrencyException|RoundingNecessaryException $e) {
            throw new MoneyCalculationException(
                sprintf(
                    'Failed to calculate money: %s',
                    $e->getMessage()
                ),
                0,
                $e,
                ['error' => $e->getMessage()]
            );
        }
    }

    /**
     * Get total price including all adjustments.
     *
     * @return Money Total price with taxes, discounts, and charges
     * @throws MoneyCalculationException If price creation fails
     */
    public function getTotalPrice(): Money
    {
        try {
            return Money::ofMinor($this->totalPrice, $this->currency);
        } catch (NumberFormatException|UnknownCurrencyException|RoundingNecessaryException $e) {
            throw new MoneyCalculationException(
                sprintf(
                    'Failed to calculate money: %s',
                    $e->getMessage()
                ),
                0,
                $e,
                ['error' => $e->getMessage()]
            );
        }
    }

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

    // ========================================================================
    // Adjustments Management
    // ========================================================================

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

    /**
     * Add pricing adjustment (tax, discount, or charge).
     * Recalculates total price and invalidates cache.
     *
     * @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->recalculateTotalPrice();

        return $this;
    }

    /**
     * Remove pricing adjustment from this item.
     * Recalculates total price and invalidates cache.
     *
     * @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->recalculateTotalPrice();
            }
        }

        return $this;
    }

    // ========================================================================
    // Protected Helper Methods
    // ========================================================================

    /**
     * Recalculate total price including all adjustments.
     * Invalidates cached summary.
     */
    protected function recalculateTotalPrice(): void
    {
        $this->invalidateCachedSummary();
        $this->totalPrice = $this->getCashier()->total()->getMinorAmount()->toInt();
    }

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