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

use BookiFlex\Core\Exception\InvalidArgumentException;
use BookiFlex\Core\Model\AccommodationType\Accommodation\AccommodationType;
use BookiFlex\Core\Model\DateUnit\DateUnit;
use BookiFlex\Core\Model\DateUnit\DateUnitTypeInterface;
use BookiFlex\Core\Model\DateUnit\Pricing\DateUnitPrice;
use BookiFlex\Core\Model\RatePlan\RatePlan;
use BookiFlex\Core\Service\DateUnit\DTO\UpdateDateUnits as UpdateDateUnitsDTO;
use BookiFlex\Core\Service\EntityValidator;
use BookiFlex\Core\Util\DateRange\WeeklyDayFilter;
use Cycle\ORM\EntityManagerInterface;

defined( 'ABSPATH' ) || exit;

/**
 * Service for updating DateUnits with new pricing system
 */
class DateUnitManager
{
    private bool $priceCancellation = false;
    private ?RatePlan $ratePlan = null;
    private ?AccommodationType $accommodationType = null;

    public function __construct(
        private readonly EntityValidator $entityValidator,
        private readonly DateUnitCreator $dateUnitsCreator,
        private readonly EntityManagerInterface $entityManager
    ) {
    }

    /**
     * @throws InvalidArgumentException
     */
    public function handle(UpdateDateUnitsDTO $command): array
    {
        // Validate and get entities
        $this->ratePlan = $this->entityValidator->getRatePlan($command->getRatePlanId());
        $this->accommodationType = $this->entityValidator->getAccommodationType($command->getAccommodationTypeId());
        $this->entityValidator->validateRatePlanSupportsAccommodation($this->ratePlan, $this->accommodationType);

        // Get or create date units
        $dateUnits = $this->dateUnitsCreator->getOrCreateDateUnits(
            $this->ratePlan,
            $this->accommodationType,
            $command->getDates()
        );

        // Index by date for quick access
        $dateUnits = array_reduce($dateUnits, function ($carry, $item) {
            $carry[$item->getDate()] = $item;
            return $carry;
        }, []);

        // Prepare prices if provided
        $prices = null;
        if ($command->getPrices()) {
            $prices = $this->preparePrices($command->getPrices());
        }

        // Update date units for selected days
        foreach ($command->getDates() as $range) {
            // All not selected days will be missed
            // Updates on past dates are not possible
            $dateRangeIterator = WeeklyDayFilter::createFilteredDateRange(
                $range['dateFrom'],
                $range['dateTo'],
                [
                    'mon' => $range['mon'],
                    'tue' => $range['tue'],
                    'wed' => $range['wed'],
                    'thu' => $range['thu'],
                    'fri' => $range['fri'],
                    'sat' => $range['sat'],
                    'sun' => $range['sun']
                ]
            );

            foreach ($dateRangeIterator as $date) {
                $key = $date->format('Y-m-d');
                if (isset($dateUnits[$key])) {
                    $this->updateDateUnit(
                        $dateUnits[$key],
                        $prices,
                        $command->getRestrictions()
                    );
                }
            }
        }

        // Persist all changes
        foreach ($dateUnits as $dateUnit) {
            $this->entityManager->persist($dateUnit);
        }
        $this->entityManager->run();

        return [
            'ratePlan' => $this->ratePlan,
            'accommodationType' => $this->accommodationType,
            'dateUnits' => array_values($dateUnits)
        ];
    }

    /**
     * Update single DateUnit with prices and restrictions
     */
    private function updateDateUnit(
        DateUnit $unit,
        ?array $prices = null,
        ?array $restrictions = null
    ): void {
        // Update restrictions
        if (isset($restrictions)) {
            $this->updateRestrictions($unit, $restrictions);
        }

        // Update price
        if (isset($prices) && !$this->priceCancellation) {
            $this->updatePrice($unit, $prices);
        }

        if ($this->priceCancellation) {
            $unit->setPrice(null);
        }
    }

    /**
     * Update DateUnit restrictions
     */
    private function updateRestrictions(DateUnit $unit, array $restrictions): void
    {
        if (array_key_exists(DateUnitTypeInterface::STOP_SALE_TYPE, $restrictions)) {
            $unit->setStopSale((bool)$restrictions[DateUnitTypeInterface::STOP_SALE_TYPE]);
        }

        if (array_key_exists(DateUnitTypeInterface::MIN_LOS_TYPE, $restrictions)) {
            $unit->setMinLengthOfStay($restrictions[DateUnitTypeInterface::MIN_LOS_TYPE]);
        }

        if (array_key_exists(DateUnitTypeInterface::MAX_LOS_TYPE, $restrictions)) {
            $unit->setMaxLengthOfStay($restrictions[DateUnitTypeInterface::MAX_LOS_TYPE]);
        }

        if (array_key_exists(DateUnitTypeInterface::MIN_LOS_ARRIVAL_TYPE, $restrictions)) {
            $unit->setMinLengthOfStayArrival($restrictions[DateUnitTypeInterface::MIN_LOS_ARRIVAL_TYPE]);
        }

        if (array_key_exists(DateUnitTypeInterface::MAX_LOS_ARRIVAL_TYPE, $restrictions)) {
            $unit->setMaxLengthOfStayArrival($restrictions[DateUnitTypeInterface::MAX_LOS_ARRIVAL_TYPE]);
        }

        if (array_key_exists(DateUnitTypeInterface::MIN_ADV_BOOKING_TYPE, $restrictions)) {
            $unit->setMinAdvanceBooking($restrictions[DateUnitTypeInterface::MIN_ADV_BOOKING_TYPE]);
        }

        if (array_key_exists(DateUnitTypeInterface::MAX_ADV_BOOKING_TYPE, $restrictions)) {
            $unit->setMaxAdvanceBooking($restrictions[DateUnitTypeInterface::MAX_ADV_BOOKING_TYPE]);
        }

        if (array_key_exists(DateUnitTypeInterface::CTA_TYPE, $restrictions)) {
            $unit->setCloseToArrival((bool)$restrictions[DateUnitTypeInterface::CTA_TYPE]);
        }

        if (array_key_exists(DateUnitTypeInterface::CTD_TYPE, $restrictions)) {
            $unit->setCloseToDeparture((bool)$restrictions[DateUnitTypeInterface::CTD_TYPE]);
        }
    }

    /**
     * Update DateUnit price
     */
    private function updatePrice(DateUnit $unit, array $prices): void
    {
        $placementRules = $this->accommodationType->getRules()->toArray();

        if ($existingPrice = $unit->getPrice()) {
            $price = $existingPrice->update($placementRules, $prices);
        } else {
            $price = DateUnitPrice::fromPlacementRules($placementRules, $prices);
        }

        $unit->setPrice($price);
    }

    /**
     * Prepare prices array from input values
     *
     * @param array $values: [
     *     variant: string,
     *     scenario: string of 'adultOnMainPlace' | 'adultOnExtraPlace' | 'childOnMainPlace' | 'childOnExtraPlace' | 'childWithoutPlace',
     *     value: string,
     *     cancel: bool
     * ]
     * @return array
     */
    private function preparePrices(array $values): array
    {
        $result = [];

        foreach ($values as $val) {
            if (!isset($val['scenario'])) {
                continue;
            }

            // if price field is empty and not set that price is not sale
            if ($val['value'] === '' && !$val['cancel']) {
                continue;
            }

            $price = $val['cancel'] ? null : abs((int)$val['value']);
            $result[$val['scenario']][$val['variant']] = $price;

            if ($val['cancel'] && $this->ratePlan->isChildRatePlan()) {
                $this->priceCancellation = true;
            }
        }

        return $result;
    }
}
