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

use BookiFlex\Core\Model\DateUnit\Pricing\DateUnitPrice;
use BookiFlex\Core\Model\Identifiable;
use BookiFlex\Core\Model\Timestamps;
use BookiFlex\Core\Util\DateTimeUtils;
use DateTimeImmutable;

defined( 'ABSPATH' ) || exit;

class DateUnit
{
    use Timestamps;
    use Identifiable;

    public const MAX_DAYS = 365;

    protected ?DateUnit $parentDateUnit = null;

    protected ?InheritancePriceRulesInterface $inheritancePriceRules = null;

    protected ?InheritanceRestrictionRulesInterface $inheritanceRestrictionRules = null;

    /**
     * Direct creation of DateUnit is not allowed
     */
    public function __construct(
        private int $ratePlanId,
        private int $accommodationTypeId,
        private DateTimeImmutable $date,
        private ?DateUnitPrice $price = null,
        private bool $stopSale = false,
        private ?int $minLengthOfStay = null,
        private ?int $maxLengthOfStay  = null,
        private ?int $minLengthOfStayArrival = null,
        private ?int $maxLengthOfStayArrival = null,
        private ?int $minAdvanceBooking = null,
        private ?int $maxAdvanceBooking = null,
        private bool $closeToArrival = false,
        private bool $closeToDeparture = false,
    ) {
        $this->createdAt = new DateTimeImmutable();
    }

    /**
     * If DateUnit has a parent DateUnit then set it. And set inheritance rules for this DateUnit
     *
     * @param DateUnit $dateUnit
     * @param InheritanceRestrictionRulesInterface $inheritanceRestrictionRules
     * @param InheritancePriceRulesInterface $inheritancePriceRules
     * @return void
     */
    public function setParentDateUnit(
        self $dateUnit,
        InheritanceRestrictionRulesInterface $inheritanceRestrictionRules,
        InheritancePriceRulesInterface $inheritancePriceRules
    ): void {
        if (!$this->equalsDate($dateUnit->getDate())
            || $dateUnit->getAccommodationTypeId() !== $this->getAccommodationTypeId()) {
            return;
        }

        $this->parentDateUnit              = $dateUnit;
        $this->inheritanceRestrictionRules = $inheritanceRestrictionRules;
        $this->inheritancePriceRules       = $inheritancePriceRules;
    }

    public function getAccommodationTypeId(): int
    {
        return $this->accommodationTypeId;
    }

    public function getRatePlanId(): int
    {
        return $this->ratePlanId;
    }

    /**
     * @return string date at Y-m-d format
     */
    public function getDate(): string
    {
        return $this->date->format('Y-m-d');
    }

    /**
     * Return Price object for current DateUnit.
     *
     * @param bool $override override parent price.
     *                       If override is false will be returned Price from current DateUnit without any overridden.
     * @return DateUnitPrice|null
     */
    public function getPrice(bool $override = true): ?DateUnitPrice
    {
        // Base case or without inheritance
        if (!$override || $this->parentDateUnit === null) {
            return $this->price;
        }

        // If own price exists - it overrides parent
        if ($this->price instanceof DateUnitPrice) {
            $this->price->setOverridden(true);
            $this->price->setInherited(true);

            $parentPrice = $this->parentDateUnit->getPrice();
            if ($parentPrice instanceof DateUnitPrice) {
                $this->price->setParent($parentPrice);
            }

            return $this->price;
        }

        // Inherit from parent
        $parentPrice = $this->parentDateUnit->getPrice();
        if ($parentPrice === null) {
            return null;
        }

        // Create inherited price with modifiers
        $price = clone $parentPrice;
        $price->setParent($parentPrice);
        $price->setOverridden(false);
        $price->setInherited(true);

        if ($this->inheritancePriceRules) {
            $price->registerModifiers($this->inheritancePriceRules);
        }

        return $price;
    }

    /**
     * Set price
     */
    public function setPrice(?DateUnitPrice $price): self
    {
        if ($price && $price->isEmpty()) {
            $this->removeOverriddenPrice();
        } else {
            $this->price = $price;
        }

        return $this;
    }

    /**
     * Remove overridden price
     */
    public function removeOverriddenPrice(): self
    {
        $this->price = null;
        return $this;
    }

    public function isStopSale(): bool
    {
        if (!isset($this->parentDateUnit)) {
            return $this->stopSale;
        }

        return $this->inheritanceRestrictionRules->inheritStopSale()
            ? $this->parentDateUnit->isStopSale()
            : $this->stopSale;
    }

    /**
     * @param bool $state
     * @return DateUnit
     */
    public function setStopSale(bool $state): self
    {
        $this->stopSale = $state;

        return $this;
    }

    public function getMinLengthOfStay(): ?int
    {
        if (!isset($this->parentDateUnit)) {
            return $this->minLengthOfStay;
        }

        return $this->inheritanceRestrictionRules->inheritMinLengthOfStay()
            ? $this->parentDateUnit->getMinLengthOfStay()
            : $this->minLengthOfStay;
    }

    /**
     * @param int|null $minLengthOfStay
     * @return DateUnit
     */
    public function setMinLengthOfStay(?int $minLengthOfStay): self
    {
        $this->minLengthOfStay = ($minLengthOfStay === null || $minLengthOfStay < 0)
            ? null
            : (min($minLengthOfStay, self::MAX_DAYS));

        return $this;
    }

    public function getMaxLengthOfStay(): ?int
    {
        if (!isset($this->parentDateUnit)) {
            return $this->maxLengthOfStay;
        }

        return $this->inheritanceRestrictionRules->inheritMaxLengthOfStay()
            ? $this->parentDateUnit->getMaxLengthOfStay()
            : $this->maxLengthOfStay;
    }

    /**
     * @param int|null $maxLengthOfStay
     * @return DateUnit
     */
    public function setMaxLengthOfStay(?int $maxLengthOfStay): self
    {
        $this->maxLengthOfStay = ($maxLengthOfStay === null || $maxLengthOfStay < 0)
            ? null
            : (min($maxLengthOfStay, self::MAX_DAYS));

        return $this;
    }

    public function getMinLengthOfStayArrival(): ?int
    {
        if (!isset($this->parentDateUnit)) {
            return $this->minLengthOfStayArrival;
        }

        return $this->inheritanceRestrictionRules->inheritMinLengthOfStayArrival()
            ? $this->parentDateUnit->getMinLengthOfStayArrival()
            : $this->minLengthOfStayArrival;
    }

    /**
     * @param int|null $minLengthOfStayArrival
     * @return DateUnit
     */
    public function setMinLengthOfStayArrival(?int $minLengthOfStayArrival): self
    {
        $this->minLengthOfStayArrival = ($minLengthOfStayArrival === null || $minLengthOfStayArrival < 0)
            ? null
            : (min($minLengthOfStayArrival, self::MAX_DAYS));

        return $this;
    }

    public function getMaxLengthOfStayArrival(): ?int
    {
        if (!isset($this->parentDateUnit)) {
            return $this->maxLengthOfStayArrival;
        }

        return $this->inheritanceRestrictionRules->inheritMaxLengthOfStayArrival()
            ? $this->parentDateUnit->getMaxLengthOfStayArrival()
            : $this->maxLengthOfStayArrival;
    }

    /**
     * @param int|null $maxLengthOfStayArrival
     * @return DateUnit
     */
    public function setMaxLengthOfStayArrival(?int $maxLengthOfStayArrival): self
    {
        $this->maxLengthOfStayArrival = ($maxLengthOfStayArrival === null || $maxLengthOfStayArrival < 0)
            ? null
            : (min($maxLengthOfStayArrival, self::MAX_DAYS));

        return $this;
    }

    public function getMinAdvanceBooking(): ?int
    {
        if (!isset($this->parentDateUnit)) {
            return $this->minAdvanceBooking;
        }

        return $this->inheritanceRestrictionRules->inheritMinAdvBooking()
            ? $this->parentDateUnit->getMinAdvanceBooking()
            : $this->minAdvanceBooking;
    }

    /**
     * @param int|null $minAdvanceBooking
     * @return DateUnit
     */
    public function setMinAdvanceBooking(?int $minAdvanceBooking): self
    {
        $this->minAdvanceBooking = ($minAdvanceBooking === null || $minAdvanceBooking < 0)
            ? null
            : (min($minAdvanceBooking, self::MAX_DAYS));

        return $this;
    }

    public function getMaxAdvanceBooking(): ?int
    {
        if (!isset($this->parentDateUnit)) {
            return $this->maxAdvanceBooking;
        }

        return $this->inheritanceRestrictionRules->inheritMaxAdvBooking()
            ? $this->parentDateUnit->getMaxAdvanceBooking()
            : $this->maxAdvanceBooking;
    }

    /**
     * @param int|null $maxAdvanceBooking
     * @return DateUnit
     */
    public function setMaxAdvanceBooking(?int $maxAdvanceBooking): self
    {
        $this->maxAdvanceBooking = ($maxAdvanceBooking === null || $maxAdvanceBooking < 0)
            ? null
            : (min($maxAdvanceBooking, self::MAX_DAYS));

        return $this;
    }

    public function isCloseToArrival(): bool
    {
        if (!isset($this->parentDateUnit)) {
            return $this->closeToArrival;
        }

        return $this->inheritanceRestrictionRules->inheritCloseToArrival()
            ? $this->parentDateUnit->isCloseToArrival()
            : $this->closeToArrival;
    }

    /**
     * @param bool $closeToArrival
     * @return DateUnit
     */
    public function setCloseToArrival(bool $closeToArrival): self
    {
        $this->closeToArrival = $closeToArrival;

        return $this;
    }

    public function isCloseToDeparture(): bool
    {
        if (!isset($this->parentDateUnit)) {
            return $this->closeToDeparture;
        }

        return $this->inheritanceRestrictionRules->inheritCloseToDeparture()
            ? $this->parentDateUnit->isCloseToDeparture()
            : $this->closeToDeparture;
    }

    /**
     * @param bool $closeToDeparture
     * @return DateUnit
     */
    public function setCloseToDeparture(bool $closeToDeparture): self
    {
        $this->closeToDeparture = $closeToDeparture;

        return $this;
    }

    /**
     * @param string $date date at Y-m-d format
     * @return bool
     */
    public function equalsDate(string $date): bool
    {
        return $date === $this->getDate();
    }

    /**
     * Return restrictions for the period between two dates
     *
     * @param DateTimeImmutable $dateFrom
     * @param DateTimeImmutable $dateTo
     * @return array
     * @throws \Exception
     */
    public function checkRestrictionsOnPeriod(DateTimeImmutable $dateFrom, DateTimeImmutable $dateTo): array
    {
        // set time to 00:00:00
        $dateFrom = new DateTimeImmutable($dateFrom->format('Y-m-d'), $dateFrom->getTimezone());
        $dateTo   = new DateTimeImmutable($dateTo->format('Y-m-d'), $dateTo->getTimezone());

        $lengthOfStay          = DateTimeUtils::lengthOfStay($dateFrom, $dateTo);
        $leftDaysBeforeArrival = DateTimeUtils::getLeftDaysBeforeArrival($dateFrom);

        $restrictions = [];

        // If anyone element has a StopSale then all set is not available
        if ($this->isStopSaleRestricted()) {
            $restrictions[] = DateUnitTypeInterface::STOP_SALE_TYPE;
        }

        // If DateUnit that equals dateStart has a CTA = true
        // OR DateUnit that equals dateEnd has a CTD = true
        // then all set is not available
        if ($this->isCloseToArrivalRestricted($dateFrom)) {
            $restrictions[] = DateUnitTypeInterface::CTA_TYPE;
        }

        if ($this->isCloseToDepartureRestricted($dateTo)) {
            $restrictions[] = DateUnitTypeInterface::CTD_TYPE;
        }

        // If anyone DateUnit has MinLengthOfStay > $length
        // OR anyone DateUnit has MaxLengthOfStay < $length
        // then all set is not available
        if ($this->isMinLengthOfStayRestricted($lengthOfStay)) {
            $restrictions[] = DateUnitTypeInterface::MIN_LOS_TYPE;
        }

        if ($this->isMaxLengthOfStayRestricted($lengthOfStay)) {
            $restrictions[] = DateUnitTypeInterface::MAX_LOS_TYPE;
        }

        // If DateUnit that equals DateStart has MinLengthOfStayOnArrival > $length
        // OR MaxLengthOfStayOnArrival < $length
        // then all set is not available
        if ($this->isMinLengthOfStayOnArrivalRestricted($lengthOfStay, $dateFrom)) {
            $restrictions[] = DateUnitTypeInterface::MIN_LOS_ARRIVAL_TYPE;
        }

        if ($this->isMaxLengthOfStayOnArrivalRestricted($lengthOfStay, $dateFrom)) {
            $restrictions[] = DateUnitTypeInterface::MAX_LOS_ARRIVAL_TYPE;
        }

        // If DateUnit that equals DateStart has MinTimeFromBookingDate > $daysBeforeArrival
        // OR MaxTimeFromBookingDate < $daysBeforeArrival
        // then all set is not available
        if ($this->isMinAdvanceBookingRestricted($leftDaysBeforeArrival, $dateFrom)) {
            $restrictions[] = DateUnitTypeInterface::MIN_ADV_BOOKING_TYPE;
        }

        if ($this->isMaxAdvanceBookingRestricted($leftDaysBeforeArrival, $dateFrom)) {
            $restrictions[] = DateUnitTypeInterface::MAX_ADV_BOOKING_TYPE;
        }

        return $restrictions;
    }

    private function isStopSaleRestricted(): bool
    {
        return $this->isStopSale();
    }

    private function isCloseToArrivalRestricted(DateTimeImmutable $arrivalDate): bool
    {
        return $this->isCloseToArrival() && $this->equalsDate($arrivalDate->format('Y-m-d'));
    }

    private function isCloseToDepartureRestricted(DateTimeImmutable $departureDate): bool
    {
        return $this->isCloseToDeparture() && $this->equalsDate($departureDate->format('Y-m-d'));
    }

    private function isMinLengthOfStayRestricted(int $length): bool
    {
        if ($this->getMinLengthOfStay() === null) {
            return false;
        }

        return ($this->getMinLengthOfStay() !== 0) && ($this->getMinLengthOfStay() > $length);
    }

    private function isMaxLengthOfStayRestricted(int $length): bool
    {
        if ($this->getMaxLengthOfStay() === null) {
            return false;
        }

        return ($this->getMaxLengthOfStay() !== 0) && ($this->getMaxLengthOfStay() < $length);
    }

    private function isMinLengthOfStayOnArrivalRestricted(int $length, DateTimeImmutable $arrivalDate): bool
    {
        if ($this->getMinLengthOfStayArrival() === null) {
            return false;
        }
        return ($this->getMinLengthOfStayArrival() !== 0)
            && $this->equalsDate($arrivalDate->format('Y-m-d'))
            && ($this->getMinLengthOfStayArrival() > $length);
    }

    private function isMaxLengthOfStayOnArrivalRestricted(int $length, DateTimeImmutable $arrivalDate): bool
    {
        if ($this->getMaxLengthOfStayArrival() === null) {
            return false;
        }

        return ($this->getMaxLengthOfStayArrival() !== 0)
            && $this->equalsDate($arrivalDate->format('Y-m-d'))
            && ($this->getMaxLengthOfStayArrival() < $length);
    }

    private function isMinAdvanceBookingRestricted(int $daysBefore, DateTimeImmutable $arrivalDate): bool
    {
        if ($this->getMinAdvanceBooking() === null) {
            return false;
        }

        return ($this->getMinAdvanceBooking() !== 0)
            && $this->equalsDate($arrivalDate->format('Y-m-d'))
            && ($this->getMinAdvanceBooking() > $daysBefore);
    }

    private function isMaxAdvanceBookingRestricted(int $daysBefore, DateTimeImmutable $arrivalDate): bool
    {
        if ($this->getMaxAdvanceBooking() === null) {
            return false;
        }

        return ($this->getMaxAdvanceBooking() !== 0)
            && $this->equalsDate($arrivalDate->format('Y-m-d'))
            && ($this->getMaxAdvanceBooking() < $daysBefore);
    }
}
