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

use BookiFlex\Core\Contract\AllocationServiceInterface;
use BookiFlex\Core\Contract\AvailabilityChecker\AvailabilityCheckerInterface;
use BookiFlex\Core\Contract\AvailabilityChecker\ExtraAvailabilityInterface;
use BookiFlex\Core\Contract\AvailabilityChecker\QuotaAvailabilityInterface;
use BookiFlex\Core\Contract\AvailabilityChecker\RatePlanAvailabilityInterface;
use BookiFlex\Core\Model\AccommodationType\Accommodation\AccommodationType;
use BookiFlex\Core\Model\DateUnit\RestrictionRulesConfigurator;
use BookiFlex\Core\Model\RatePlan\RatePlan;
use BookiFlex\Core\Repository\AccommodationTypeRepository;
use BookiFlex\Core\Repository\RatePlanRepository;
use BookiFlex\Core\Service\DateUnit\DateUnitFinder;
use BookiFlex\Core\ValueObject\AccommodationSelection;
use BookiFlex\Core\ValueObject\GuestConfiguration;
use BookiFlex\Core\ValueObject\StayPeriod;
use Exception;

defined( 'ABSPATH' ) || exit;

/**
 * Check availability of accommodation by specified parameters like:
 * - accommodation type
 * - rate plan
 * - check-in and check-out dates
 * - number of adults and children
 * - extras
 */
class AvailabilityChecker implements AvailabilityCheckerInterface
{
    private ?AccommodationType $accommodationType = null;
    private ?RatePlan $ratePlan = null;
    private array $errors = [];
    private int $availableQuota = 0;
    private array $availableExtras = [];

    public function __construct(
        private readonly AccommodationTypeRepository $accommodationTypeRepository,
        private readonly RatePlanRepository $ratePlanRepository,
        private readonly DateUnitFinder $dateUnitsFinder,
        private readonly AllocationServiceInterface $placementService,
        private readonly ?QuotaAvailabilityInterface $quotaAvailabilityService = null,
        private readonly ?ExtraAvailabilityInterface $extraAvailabilityService = null,
        private readonly ?RatePlanAvailabilityInterface $ratePlanAvailabilityService = null,
    ) {
    }

    public function check(
        StayPeriod $stayPeriod,
        AccommodationSelection $accommodationSelection,
        GuestConfiguration $guestConfiguration,
        int $quantity = 1,
        string $checkInTime = '14:00'
    ): void {
        // Reset state
        $this->errors = [];
        $this->availableQuota = 0;
        $this->availableExtras = [];
        $this->accommodationType = null;
        $this->ratePlan = null;

        // Check quota availability
        $this->availableQuota = $this->checkQuotaAvailability($stayPeriod, $accommodationSelection);
        if ($this->availableQuota < $quantity) {
            $this->errors['insufficient_availability'] = 'Not enough available accommodations';
        }

        // Check accommodation type
        try {
            $this->accommodationType = $this->accommodationTypeRepository->findByPK($accommodationSelection->getAccommodationTypeId());
            $this->checkAccommodationType($guestConfiguration);
        } catch (Exception $e) {
            // Continue checking other conditions
        }

        // Check rate plan
        try {
            $this->ratePlan = $this->ratePlanRepository->findByPK($accommodationSelection->getRatePlanId());
            $this->checkRatePlan();
            $this->checkRatePlanAvailability($stayPeriod, $accommodationSelection, $guestConfiguration);
        } catch (Exception $e) {
            // Continue checking other conditions
        }

        // Check available payment types
        if ($this->ratePlan !== null) {
            $paymentTypes = $this->ratePlan->getAvailablePaymentTypes($this->getCheckInDateTime($stayPeriod, $checkInTime));
            if (empty($paymentTypes)) {
                $this->errors['no_payment_types'] = 'Not enough available payment types';
            }
        }

        // Check extras
        try {
            $this->availableExtras = $this->checkExtraAvailability($stayPeriod, $accommodationSelection, $guestConfiguration);
        } catch (Exception $e) {
            // Continue checking other conditions
        }

        // Check date units
        try {
            $this->checkDateUnits($stayPeriod, $accommodationSelection);
        } catch (Exception $e) {
            // Continue checking other conditions
        }
    }

    public function isAvailable(): bool
    {
        return empty($this->errors);
    }

    /**
     * Get error messages as associative array
     *
     * @return array<string, string> Associative array where key is error code and value is error message
     */
    public function getErrorMessages(): array
    {
        return $this->errors;
    }

    public function getAvailableQuota(): int
    {
        return $this->availableQuota;
    }

    public function getAvailableExtras(): array
    {
        return $this->availableExtras;
    }

    private function checkQuotaAvailability(StayPeriod $stayPeriod, AccommodationSelection $accommodationSelection): int
    {
        if ($this->quotaAvailabilityService) {
            return $this->quotaAvailabilityService->getQuota(
                new StayPeriod($stayPeriod->getCheckInDate(), $stayPeriod->getCheckOutDate()),
                $accommodationSelection->getAccommodationTypeId()
            );
        }

        return 0;
    }

    private function checkAccommodationType(GuestConfiguration $guestConfiguration): void
    {
        if (!$this->accommodationType) {
            $this->errors['accommodation_type_not_found'] = 'Accommodation type not found';
            return;
        }

        if (!$this->accommodationType->isActive()) {
            $this->errors['accommodation_type_inactive'] = 'Accommodation type is not active';
            return;
        }

        if (!$this->placementService->canAccommodate($this->accommodationType, $guestConfiguration)) {
            $this->errors['accommodation_type_guest_capacity'] = 'Accommodation type can not accommodate specified number of guests';
        }
    }

    private function checkRatePlan(): void
    {
        if (!$this->ratePlan) {
            $this->errors['rate_plan_not_found'] = 'Rate plan not found';
            return;
        }

        if (!$this->ratePlan->isActive()) {
            $this->errors['rate_plan_inactive'] = 'Rate plan is not active';
            return;
        }

        if ($this->accommodationType && !$this->ratePlan->supportAccommodationType($this->accommodationType->getId())) {
            $this->errors['rate_plan_wrong_accommodation'] = 'Rate plan is not available for specified accommodation type';
        }
    }

    private function checkRatePlanAvailability(
        StayPeriod $stayPeriod,
        AccommodationSelection $accommodationSelection,
        GuestConfiguration $guestConfiguration
    ): void {
        if ($this->ratePlanAvailabilityService) {
            $isAvailable = $this->ratePlanAvailabilityService->checkRatePlanAvailability($stayPeriod, $accommodationSelection, $guestConfiguration);
            if (!$isAvailable) {
                $this->errors['rate_plan_unavailable_dates'] = 'Rate plan is not available for specified dates';
            }
        }
    }

    private function checkExtraAvailability(
        StayPeriod $stayPeriod,
        AccommodationSelection $accommodationSelection,
        GuestConfiguration $guestConfiguration
    ): array {
        if ($this->extraAvailabilityService) {
            return $this->extraAvailabilityService->checkExtraAvailability($stayPeriod, $accommodationSelection, $guestConfiguration);
        }

        return [];
    }

    private function checkDateUnits(StayPeriod $stayPeriod, AccommodationSelection $accommodationSelection): void
    {
        try {
            $dateUnitsInformation = $this->dateUnitsFinder->find(
                $stayPeriod,
                $accommodationSelection,
                new RestrictionRulesConfigurator()
            );

            if (count($dateUnitsInformation->getDateUnits()) === 0 || !$dateUnitsInformation->isPeriodSequenced()) {
                $this->errors['no_date_units'] = 'No available date units found';
            }
        } catch (Exception $e) {
            $this->errors['date_units_check_error'] = 'Error checking date units availability';
        }
    }

    private function getCheckInDateTime(StayPeriod $stayPeriod, string $checkInTime): \DateTimeImmutable
    {
        return new \DateTimeImmutable(
            $stayPeriod->getCheckInDate()->format('Y-m-d') . ' ' . $checkInTime
        );
    }
}
