<?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;

use BookiFlex\Core\Exception\InvalidArgumentException;
use BookiFlex\Core\Exception\LogicException;
use BookiFlex\Core\Model\AccommodationType\Accommodation\AccommodationType;
use BookiFlex\Core\Model\AccommodationType\Exception\NotAccommodateException;
use BookiFlex\Core\Model\DateUnit\Calculator\CalculatedPriceResult;
use BookiFlex\Core\Model\DateUnit\Calculator\PriceCalculationOrchestrator;
use BookiFlex\Core\Model\DateUnit\Calculator\PriceCalculatorFactory;
use BookiFlex\Core\Model\DateUnit\Calculator\PriceCalculatorInterface;
use BookiFlex\Core\Model\DateUnit\Exception\PriceNotSetByDateException;
use BookiFlex\Core\Model\DateUnit\RestrictionRulesConfigurator;
use BookiFlex\Core\Model\RatePlan\RatePlan;
use BookiFlex\Core\Contract\AllocationServiceInterface;
use BookiFlex\Core\Service\ConfigurationService;
use BookiFlex\Core\Service\DateUnit\DateUnitFinder;
use BookiFlex\Core\Service\EntityValidator;
use BookiFlex\Core\Service\StayCostCalculator\Strategy\CalculatorStrategyResolver;
use BookiFlex\Core\Service\StayCostCalculator\ValueObject\StayCostByDate;
use BookiFlex\Core\Service\StayCostCalculator\ValueObject\StayCostCalculatorResult;
use BookiFlex\Core\ValueObject\AccommodationSelection;
use BookiFlex\Core\ValueObject\GuestConfiguration;
use BookiFlex\Core\ValueObject\StayPeriod;
use Psr\Log\LoggerInterface;

defined( 'ABSPATH' ) || exit;

/**
 * Stay cost calculation service
 * Orchestrates calculation process considering various strategies and adjustments
 */
class StayCostCalculator
{
    private ?LoggerInterface $logger;

    public function __construct(
        private readonly EntityValidator $entityValidator,
        private readonly DateUnitFinder $dateUnitsFinder,
        private readonly PriceCalculatorFactory $priceCalculatorFactory,
        private readonly AllocationServiceInterface $allocationService,
        private readonly ConfigurationService $configurationService,
        private readonly ?CalculatorStrategyResolver $strategyResolver = null,
        ?LoggerInterface $logger = null
    ) {
        $this->logger = $logger;
    }

    /**
     * Calculate stay cost
     * @throws InvalidArgumentException
     */
    public function calculate(
        StayPeriod $stayPeriod,
        AccommodationSelection $accommodationSelection,
        GuestConfiguration $guestConfiguration,
        ?RestrictionRulesConfigurator $restrictionRules = null,
        ?string $calculatorStrategy = null
    ): StayCostCalculatorResult {
        $ratePlan = $this->entityValidator->getRatePlan($accommodationSelection->getRatePlanId());
        $accommodationType = $this->entityValidator->getAccommodationTypeFromRatePlan($ratePlan, $accommodationSelection->getAccommodationTypeId());

        $context = $this->buildContext(
            $stayPeriod,
            $ratePlan,
            $accommodationType,
            $guestConfiguration
        );

        $finderResult = null;

        try {
            // todo: add cache
            // Get DateUnits for period
            $finderResult = $this->dateUnitsFinder->find(
                $stayPeriod,
                new AccommodationSelection($accommodationType->getId(), $ratePlan->getId()),
                $restrictionRules
            );

            // Create accommodation scheme
            $allocationScheme = $this->allocationService->createScheme(
                $accommodationType,
                $guestConfiguration
            );

            // Determine calculation strategy
            $calculator = $this->resolveCalculator($calculatorStrategy, $accommodationType, $ratePlan);

            // Create orchestrator with selected strategy
            $orchestrator = new PriceCalculationOrchestrator($calculator);

            // Execute calculation
            $calculatedPrice = $orchestrator->calculate(
                $allocationScheme,
                $finderResult->getDateUnits()
            );

            // Create result
            return $this->createSuccessResult(
                $calculatedPrice,
                $finderResult->getActualRestrictions(),
                $context
            );

        } catch (PriceNotSetByDateException $e) {
            $this->logger?->debug('Price not set', [
                'exception' => $e->getMessage(),
                'context' => $context
            ]);
            return $this->createErrorResult(
                'price_not_set',
                $finderResult !== null ? $finderResult->getActualRestrictions() : [],
                $context,
                $e->getMessage()
            );

        } catch (NotAccommodateException $e) {
            $this->logger?->debug('Cannot accommodate guests', [
                'exception' => $e->getMessage(),
                'context' => $context
            ]);
            return $this->createErrorResult(
                'cannot_accommodate',
                $finderResult !== null ? $finderResult->getActualRestrictions() : [],
                $context,
                $e->getMessage()
            );

        } catch (LogicException $e) {
            $this->logger?->error('Logic error in calculation', [
                'exception' => $e->getMessage(),
                'context' => $context
            ]);
            return $this->createErrorResult(
                'calculation_error',
                $finderResult !== null ? $finderResult->getActualRestrictions() : [],
                $context,
                $e->getMessage()
            );

        } catch (\Throwable $e) {
            $this->logger?->error('Unexpected error in calculation', [
                'exception' => $e->getMessage(),
                'context' => $context
            ]);
            throw new \RuntimeException('Failed to calculate stay cost', 0, $e);
        }
    }

    /**
     * Determine calculator for calculation
     */
    private function resolveCalculator(
        ?string $strategyName,
        AccommodationType $accommodationType,
        RatePlan $ratePlan
    ): PriceCalculatorInterface {
        // If specific strategy specified
        if ($strategyName !== null) {
            $calculator = $this->priceCalculatorFactory->get($strategyName);
            if ($calculator !== null) {
                return $calculator;
            }
        }

        // Use resolver if exists
        if ($this->strategyResolver !== null) {
            return $this->strategyResolver->resolve($accommodationType, $ratePlan);
        }

        // By default
        return $this->priceCalculatorFactory->getDefault();
    }

    /**
     * Build context for calculation
     */
    private function buildContext(
        StayPeriod $stayPeriod,
        RatePlan $ratePlan,
        AccommodationType $accommodationType,
        GuestConfiguration $guestConfiguration
    ): array {
        return [
            'checkInDate' => $stayPeriod->getCheckInDate()->format('Y-m-d'),
            'checkOutDate' => $stayPeriod->getCheckOutDate()->format('Y-m-d'),
            'lengthOfStay' => $stayPeriod->getLengthOfStay(),
            'ratePlanId' => $ratePlan->getId(),
            'ratePlanCode' => $ratePlan->getCode(),
            'ratePlanName' => $ratePlan->getName(),
            'accommodationTypeId' => $accommodationType->getId(),
            'accommodationType' => $accommodationType->getShortCode(),
            'accommodationName' => $accommodationType->getName(),
            'adults' => $guestConfiguration->getAdults(),
            'children' => $guestConfiguration->getChildren(),
            'totalGuests' => $guestConfiguration->getTotalGuests(),
        ];
    }

    /**
     * Create successful result
     */
    private function createSuccessResult(
        CalculatedPriceResult $calculatedPrice,
        array $restrictions,
        array $context
    ): StayCostCalculatorResult {
        $currency = $this->configurationService->getDefaultCurrency();

        // Transform daily prices to DTO
        $dailyCosts = [];
        foreach ($calculatedPrice->getDailyPrices() as $dayPrice) {
            $dailyCosts[] = StayCostByDate::fromCalculatedPrice(
                $dayPrice,
                $currency,
                $context
            );
        }

        return StayCostCalculatorResult::success(
            $currency,
            $dailyCosts,
            $calculatedPrice->getOriginalTotalPrice()?->toBrickMoney($currency),
            $restrictions,
            $context
        );
    }

    /**
     * Create error result
     */
    private function createErrorResult(
        string $errorType,
        array $restrictions,
        array $context,
        ?string $errorMessage = null
    ): StayCostCalculatorResult {
        $currency = $this->configurationService->getDefaultCurrency();

        return StayCostCalculatorResult::error(
            $currency,
            $errorType,
            $restrictions,
            $context,
            $errorMessage
        );
    }
}
