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

use BookiFlex\Core\Contract\AllocationServiceInterface;
use BookiFlex\Core\Contract\AvailabilityChecker\QuotaAvailabilityInterface;
use BookiFlex\Core\Model\AccommodationType\Accommodation\AccommodationType;
use BookiFlex\Core\Repository\AccommodationTypeRepository;
use BookiFlex\Core\Service\SuitableAccommodationType\DTO\SuitableAccommodationTypeResult;
use BookiFlex\Core\Service\SuitableAccommodationType\Event\SuitableAccommodationTypesAreFound;
use BookiFlex\Core\ValueObject\GuestConfiguration;
use BookiFlex\Core\ValueObject\StayPeriod;
use Psr\EventDispatcher\EventDispatcherInterface;

defined( 'ABSPATH' ) || exit;

/**
 * Search Accommodation types by availability and capacity.
 * And returns array of objects with Accommodation types and available quota of units.
 */
class SuitableAccommodationType
{
    public function __construct(
        private readonly AccommodationTypeRepository $accommodationTypeRepository,
        private readonly AllocationServiceInterface $allocationService,
        private readonly ?QuotaAvailabilityInterface $quotaAvailability = null,
        private readonly ?EventDispatcherInterface $dispatcher = null
    ) {
    }

    /**
     * Search Accommodation types by activity and capacity.
     * If adults is not set then return all active Accommodation types.
     *
     * @param int|null $adults  Number of adults OR null if not specified
     * @param array $childrenAges   Number of children by age
     * @param array $accommodationTypeIds        Restrict by AccommodationType IDs
     *
     * @return SuitableAccommodationTypeResult[]
     */
    public function search(
        ?int  $adults,
        array $childrenAges = [],
        array $accommodationTypeIds = [],
        mixed $context = null
    ): array {
        $accommodationTypes = $this->getAccommodationTypes($accommodationTypeIds, $adults, $childrenAges);
        $checkInOutDates    = $this->getCheckInOutDates($context);

        $suitableAccommodationTypeResults = [];
        foreach ($accommodationTypes as $entry) {
            $accommodationType = $entry['accommodationType'];
            $quota = ($this->quotaAvailability && $checkInOutDates)
                ? $this->quotaAvailability->getQuota(
                    new StayPeriod($checkInOutDates['checkInDate'], $checkInOutDates['checkOutDate']),
                    $accommodationType->getId()
                )
                : 1;
            $suitableAccommodationTypeResults[] = new SuitableAccommodationTypeResult(
                $accommodationType,
                $quota,
                $entry['allocationScheme']
            );
        }

        // filter Accommodation types or update a quota
        if ($this->dispatcher) {
            $event                            = $this->dispatcher->dispatch(new SuitableAccommodationTypesAreFound($suitableAccommodationTypeResults, $context));
            $suitableAccommodationTypeResults = $event->getSuitableAccommodationTypes();
        }

        return $suitableAccommodationTypeResults;
    }

    /**
     * Get Accommodation types by IDs.
     *
     * Each entry pairs an active AccommodationType with the concrete AllocationScheme
     * that proves the requested guests fit. The scheme is null when no explicit guest
     * count was provided (no fitting check is performed in that case).
     *
     * @param array $ids
     * @param int|null $adults
     * @param array $childrenAges
     *
     * @return array<int, array{accommodationType: AccommodationType, allocationScheme: \BookiFlex\Core\Model\AccommodationType\Scheme\AllocationScheme|null}>
     */
    private function getAccommodationTypes(array $ids, ?int $adults = null, array $childrenAges = []): array
    {
        $iterator           = $this->accommodationTypeRepository->findAll(['ids' => $ids]);
        $accommodationTypes = [];

        /** @var AccommodationType $accommodationType */
        foreach ($iterator as $accommodationType) {
            if (!$accommodationType->isActive()) {
                continue;
            }

            if (empty($adults)) {
                $accommodationTypes[] = [
                    'accommodationType' => $accommodationType,
                    'allocationScheme'  => null,
                ];
                continue;
            }

            // Building the scheme both validates the fit and captures how the guests are placed,
            // so we reuse it downstream instead of recomputing the allocation.
            try {
                $allocationScheme = $this->allocationService->createScheme(
                    $accommodationType,
                    new GuestConfiguration($adults, $childrenAges)
                );
            } catch (\Throwable $e) {
                // Guests do not fit this accommodation type.
                continue;
            }

            $accommodationTypes[] = [
                'accommodationType' => $accommodationType,
                'allocationScheme'  => $allocationScheme,
            ];
        }

        return $accommodationTypes;
    }

    private function getCheckInOutDates(mixed $context): ?array
    {
        if (!is_object($context)) {
            return null;
        }

        if (!method_exists($context, 'getCheckInDate') || !method_exists($context, 'getCheckOutDate')) {
            return null;
        }

        $checkInDate  = $context->getCheckInDate();
        $checkOutDate = $context->getCheckOutDate();

        if (!$checkInDate instanceof \DateTimeImmutable || !$checkOutDate instanceof \DateTimeImmutable) {
            return null;
        }

        return [
            'checkInDate'  => $checkInDate,
            'checkOutDate' => $checkOutDate,
        ];
    }
}
