<?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\Service\StayCostCalculator\ValueObject;

use BookiFlex\Core\Contract\Adjustment\{AdjustableInterface,
    AdjustmentInterface};
use BookiFlex\Core\Contract\Taxation\TaxableInterface;
use BookiFlex\Core\Exception\MoneyCalculationException;
use BookiFlex\Core\Service\CashierFactory;
use BookiFlex\Core\ValueObject\Summary;
use BookiFlex\Core\ValueObject\Totalizer;
use Brick\Math\RoundingMode;
use Brick\Money\Exception\MoneyMismatchException;
use Brick\Money\Money;
use Vaened\PriceEngine\Cashier;

defined( 'ABSPATH' ) || exit;

/**
 * Stay cost calculation result
 *
 * Supports two levels of adjustments:
 * 1. Daily adjustments - applied to each day separately
 * 2. Booking adjustments - applied to the entire period
 */
final class StayCostCalculatorResult implements AdjustableInterface, TaxableInterface
{
    private string $currency;
    /** @var StayCostByDate[] */
    private array $dailyCosts;
    private ?Money $originalBaseAmount;  // Original base amount
    private array $restrictions;
    private array $context;
    private ?string $errorType;
    private ?string $errorMessage;

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

    private ?Summary $cachedDailySummary = null;
    private ?Summary $cachedCompleteSummary = null;
    private ?Cashier $cachedCashier = null;

    private function __construct(
        string $currency,
        array $dailyCosts,
        ?Money $originalBaseAmount,
        array $restrictions,
        array $context,
        ?string $errorType = null,
        ?string $errorMessage = null
    ) {
        $this->currency = $currency;
        $this->dailyCosts = $dailyCosts;
        $this->originalBaseAmount = $originalBaseAmount;
        $this->restrictions = $restrictions;
        $this->context = $context;
        $this->errorType = $errorType;
        $this->errorMessage = $errorMessage;
    }

    /**
     * Create successful result
     */
    public static function success(
        string $currency,
        array $dailyCosts,
        ?Money $originalBaseAmount,
        array $restrictions,
        array $context
    ): self {
        return new self(
            $currency,
            $dailyCosts,
            $originalBaseAmount,
            $restrictions,
            $context
        );
    }

    /**
     * Create error result
     */
    public static function error(
        string $currency,
        string $errorType,
        array $restrictions,
        array $context,
        ?string $errorMessage = null
    ): self {
        return new self(
            $currency,
            [],
            null,
            $restrictions,
            $context,
            $errorType,
            $errorMessage
        );
    }

    // ============================================
    // Main methods for price work
    // ============================================

    /**
     * Get summary for daily expenses only
     * Aggregates all daily summaries without applying booking adjustments
     * @throws MoneyCalculationException
     */
    public function getDailyAggregatedSummary(): ?Summary
    {
        if (!$this->isSuccess() || empty($this->dailyCosts)) {
            return null;
        }

        if ($this->cachedDailySummary === null) {
            $this->cachedDailySummary = Totalizer::of($this->aggregateDailyCashiers(), $this->currency)->summary();
        }

        return $this->cachedDailySummary;
    }

    /**
     * Get complete summary with all adjustments
     * Includes both daily adjustments and booking adjustments
     */
    public function getCompleteSummary(): ?Summary
    {
        if (!$this->isSuccess()) {
            return null;
        }

        if ($this->cachedCompleteSummary === null) {
            $this->cachedCompleteSummary = $this->calculateCompleteSummary();
        }

        return $this->cachedCompleteSummary;
    }

    /**
     * Aggregates daily summaries into one common summary
     * @return Cashier[]
     */
    public function aggregateDailyCashiers(): array
    {
        $cashiers = [];
        foreach ($this->dailyCosts as $dailyCost) {
            $cashiers[$dailyCost->getDate()->format('Y-m-d')] = $dailyCost->getCashier();
        }

        return $cashiers;
    }

    public function getCashier(): Cashier
    {
        if ($this->cachedCashier === null) {
            $this->cachedCashier = CashierFactory::make(Money::zero($this->currency), $this->bookingAdjustments);
        }
        return $this->cachedCashier;
    }

    /**
     * Calculates complete summary with booking adjustments
     * @throws MoneyCalculationException|\Brick\Money\Exception\MoneyMismatchException
     */
    private function calculateCompleteSummary(): Summary
    {
        $bookingSummaries = $this->getCashier();
        $aggregateDailySummaries = $this->getDailyAggregatedSummary();

        return new Summary(
            $bookingSummaries->subtotal()->net()->plus($aggregateDailySummaries->subtotal()),
            $bookingSummaries->taxes()->total()->plus($aggregateDailySummaries->totalTaxes()),
            $bookingSummaries->charges()->total()->plus($aggregateDailySummaries->totalCharges()),
            $bookingSummaries->discounts()->total()->plus($aggregateDailySummaries->totalDiscounts()),
            $bookingSummaries->total()->plus($aggregateDailySummaries->total()),
        );
    }

    // ============================================
    // Methods for working with adjustments
    // ============================================

    /**
     * Add booking level adjustment
     * These adjustments are applied to the entire period
     */
    public function addBookingAdjustment(AdjustmentInterface $adjustment): self
    {
        $this->bookingAdjustments[$adjustment->getCode()] = $adjustment;
        $this->cachedCompleteSummary = null;
        $this->cachedCashier = null;
        return $this;
    }

    /**
     * Add adjustment to all days
     * These adjustments are applied to each day separately
     */
    public function addDailyAdjustment(AdjustmentInterface $adjustment): self
    {
        foreach ($this->dailyCosts as $dailyCost) {
            $dailyCost->addAdjustment(clone $adjustment);
        }
        $this->cachedDailySummary = null;
        $this->cachedCompleteSummary = null;
        return $this;
    }

    /**
     * Add adjustment to specific days
     * @param string[] $dates Array of dates in Y-m-d format
     */
    public function addAdjustmentToDates(AdjustmentInterface $adjustment, array $dates): self
    {
        foreach ($this->dailyCosts as $dailyCost) {
            if (in_array($dailyCost->getDateString(), $dates, true)) {
                $dailyCost->addAdjustment(clone $adjustment);
            }
        }
        $this->cachedDailySummary = null;
        $this->cachedCompleteSummary = null;
        return $this;
    }

    /**
     * Get booking level adjustments
     */
    public function getBookingAdjustments(): array
    {
        return $this->bookingAdjustments;
    }

    /**
     * Get all daily adjustments (aggregated)
     */
    public function getAllDailyAdjustments(): array
    {
        $adjustments = [];
        foreach ($this->dailyCosts as $dailyCost) {
            foreach ($dailyCost->getAdjustments() as $code => $adjustment) {
                $adjustments[$dailyCost->getDate()->format('Y-m-d')][$code] = $adjustment;
            }
        }
        return $adjustments;
    }

    // ============================================
    // AdjustableInterface implementation
    // ============================================

    /**
     * Returns base price for calculating booking adjustments
     * This is the sum of final prices of all days (already with their adjustments)
     */
    public function getBasePrice(): Money
    {
        if (!$this->isSuccess() || empty($this->dailyCosts)) {
            return Money::zero($this->currency);
        }

        return $this->getDailyAggregatedSummary()->total();
    }

    /**
     * @deprecated Use addBookingAdjustment() for clarity
     */
    public function addAdjustment(AdjustmentInterface $adjustment): self
    {
        return $this->addBookingAdjustment($adjustment);
    }

    /**
     * @deprecated Use getBookingAdjustments() for clarity
     */
    public function getAdjustments(): array
    {
        return $this->bookingAdjustments;
    }

    // ============================================
    // Other methods unchanged
    // ============================================

    /**
     * @throws MoneyCalculationException
     */
    public function getBaseAmount(): Money
    {
        return $this->getDailyAggregatedSummary()->subtotal();
    }

    public function getOriginalBaseAmount(): ?Money
    {
        return $this->originalBaseAmount;
    }

    /**
     * @throws MoneyCalculationException
     * @throws MoneyMismatchException
     */
    public function hasInheritanceDiscount(): bool
    {
        if ($this->originalBaseAmount === null) {
            return false;
        }

        return $this->getBaseAmount()->isLessThan($this->originalBaseAmount);
    }

    /**
     * @throws MoneyCalculationException
     * @throws MoneyMismatchException
     */
    public function getInheritanceDiscount(): ?Money
    {
        if (!$this->hasInheritanceDiscount()) {
            return null;
        }

        return $this->originalBaseAmount->minus($this->getBaseAmount());
    }

    public function getInheritanceDiscountPercentage(): ?float
    {
        if (!$this->hasInheritanceDiscount() || $this->originalBaseAmount->isZero()) {
            return null;
        }

        $discount = $this->getInheritanceDiscount();
        return $discount->dividedBy($this->originalBaseAmount->getAmount(), RoundingMode::HALF_UP)
            ->multipliedBy(100)
            ->getAmount()
            ->toFloat();
    }

    /**
     * Return final price (with all taxes, fees and discounts)
     * @return Money|null
     */
    public function getFinalPrice(): ?Money
    {
        $summary = $this->getCompleteSummary();
        return $summary?->total();
    }

    public function getDailyCosts(): array
    {
        return $this->dailyCosts;
    }

    public function getCostForDate(string $date): ?StayCostByDate
    {
        foreach ($this->dailyCosts as $dailyCost) {
            if ($dailyCost->getDateString() === $date) {
                return $dailyCost;
            }
        }
        return null;
    }

    public function getNightsCount(): int
    {
        return count($this->dailyCosts);
    }

    /**
     * @throws MoneyCalculationException
     */
    public function getAverageNightBasePrice(): ?Money
    {
        if ($this->getBaseAmount()->isZero() || $this->getNightsCount() === 0) {
            return null;
        }

        return $this->getBaseAmount()->dividedBy($this->getNightsCount());
    }

    public function getAverageNightFinalPrice(): ?Money
    {
        $finalPrice = $this->getFinalPrice();
        if ($finalPrice === null || $this->getNightsCount() === 0) {
            return null;
        }

        return $finalPrice->dividedBy($this->getNightsCount());
    }

    public function isSuccess(): bool
    {
        return $this->errorType === null;
    }

    public function hasError(): bool
    {
        return $this->errorType !== null;
    }

    public function getErrorType(): ?string
    {
        return $this->errorType;
    }

    public function getErrorMessage(): ?string
    {
        return $this->errorMessage;
    }

    public function getRestrictions(): array
    {
        return $this->restrictions;
    }

    public function hasRestrictions(): bool
    {
        return !empty($this->restrictions);
    }

    public function getContext(): array
    {
        return $this->context;
    }

    public function getCurrency(): string
    {
        return $this->currency;
    }

    // ============================================
    // TaxableInterface implementation
    // ============================================

    public function getTaxableAmount(): Money
    {
        return $this->getBasePrice(); // Taxes from sum of all days
    }

    public function getTaxableType(): string
    {
        return 'accommodation_booking'; // Changed for clarity
    }

    public function getTaxContext(): array
    {
        return array_merge($this->context, [
            'nights' => $this->getNightsCount(),
            'hasInheritanceDiscount' => $this->hasInheritanceDiscount(),
            'inheritanceDiscountPercentage' => $this->getInheritanceDiscountPercentage(),
            'hasRestrictions' => $this->hasRestrictions(),
            'hasDailyAdjustments' => !empty($this->getAllDailyAdjustments()),
            'hasBookingAdjustments' => !empty($this->bookingAdjustments),
        ]);
    }

    /**
     * Convert to array
     */
    public function toArray(): array
    {
        $dailySummary = $this->getDailyAggregatedSummary();
        $completeSummary = $this->getCompleteSummary();

        $result = [
            'success' => $this->isSuccess(),
            'errorType' => $this->errorType,
            'errorMessage' => $this->errorMessage,
            'currency' => $this->currency,

            // Base amounts
            'baseAmount' => $this->getBaseAmount()->getAmount(),
            'originalBaseAmount' => $this->originalBaseAmount?->getAmount(),

            // Inheritance discount information
            'inheritance' => [
                'hasDiscount' => $this->hasInheritanceDiscount(),
                'discountAmount' => $this->getInheritanceDiscount()?->getAmount(),
                'discountPercentage' => $this->getInheritanceDiscountPercentage(),
            ],

            // Stay information
            'nights' => $this->getNightsCount(),
            'averageNightBasePrice' => $this->getAverageNightBasePrice()?->getAmount(),
            'averageNightFinalPrice' => $this->getAverageNightFinalPrice()?->getAmount(),

            // Restrictions
            'restrictions' => $this->restrictions,

            // Daily expenses
            'dailyCosts' => array_map(fn($cost) => $cost->toArray(), $this->dailyCosts),

            // Adjustments
            'adjustments' => [
                'daily' => array_keys($this->getAllDailyAdjustments()),
                'booking' => array_keys($this->bookingAdjustments),
            ],

            // Context
            'context' => $this->context,
        ];

        // Add summaries only if calculation successful
        if ($dailySummary !== null) {
            $result['dailySummary'] = [
                'subtotal' => $dailySummary->subtotal()->getAmount(),
                'taxes' => $dailySummary->totalTaxes()->getAmount(),
                'charges' => $dailySummary->totalCharges()->getAmount(),
                'discounts' => $dailySummary->totalDiscounts()->getAmount(),
                'total' => $dailySummary->total()->getAmount(),
            ];
        }

        if ($completeSummary !== null) {
            $result['completeSummary'] = [
                'subtotal' => $completeSummary->subtotal()->getAmount(),
                'taxes' => $completeSummary->totalTaxes()->getAmount(),
                'charges' => $completeSummary->totalCharges()->getAmount(),
                'discounts' => $completeSummary->totalDiscounts()->getAmount(),
                'total' => $completeSummary->total()->getAmount(),
            ];
        }

        return $result;
    }
}
