<?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\Api\Transformer\Offer;

use BookiFlex\Core\Model\AccommodationType\Guest\ChildGuest;
use BookiFlex\Core\Model\AccommodationType\Scheme\Allocation;
use BookiFlex\Core\Model\AccommodationType\Scheme\AllocationScheme;
use BookiFlex\Core\Model\PaymentType\PaymentType;
use BookiFlex\Core\Model\RatePlan\RatePlan;
use BookiFlex\Core\Service\OfferFinder\DTO\OfferResult;
use BookiFlex\Core\Service\StayCostCalculator\ValueObject\StayCostByDate;
use BookiFlex\Core\Service\StayCostCalculator\ValueObject\StayCostCalculatorResult;
use Brick\Math\RoundingMode;
use DateTimeImmutable;

defined( 'ABSPATH' ) || exit;

class OffersTransformer
{
    /**
     * @param OfferResult[] $items
     */
    public function transform(array $items, DateTimeImmutable $checkInDateTime): array
    {
        $ratePlans          = [];
        $accommodationTypes = [];
        $quota              = [];

        $grouped = [];
        foreach ($items as $item) {
            if (!isset($accommodationTypes[$item->getAccommodationType()->getId()])) {
                $accommodationTypes[$item->getAccommodationType()->getId()] = $item->getAccommodationType();
            }

            if (!isset($quota[$item->getAccommodationType()->getId()])) {
                $quota[$item->getAccommodationType()->getId()] = $item->getQuota();
            }

            if ($item->getRatePlan() && !isset($ratePlans[$item->getRatePlan()->getId()])) {
                $ratePlans[$item->getRatePlan()->getId()] = $item->getRatePlan();
            }

            $grouped[$item->getAccommodationType()->getId()][$item->getRatePlan()?->getId()][] = [
                'restrictions'    => $item->getRestrictions(),
                'stayCost'        => $item->getStayCost(),
                'scenario'        => $item->getScenario(),
                'allocationScheme' => $item->getAllocationScheme(),
            ];
        }

        $offers = [];
        foreach ($grouped as $accommodationTypeId => $ratePlanGroup) {
            $accommodationType = $accommodationTypes[$accommodationTypeId];
            $preparedRatePlans = $this->prepareOffers($ratePlanGroup, $ratePlans, $checkInDateTime, $accommodationTypeId);
            $offers[]          = [
                'roomQuantity'      => !empty($preparedRatePlans) ? $quota[$accommodationTypeId] : 0,
                'accommodationType' => [
                    'id'            => $accommodationType->getId(),
                    'code'          => $accommodationType->getShortCode(),
                    'name'          => $accommodationType->getName(),
                    'type'          => $accommodationType->getAccommodationCategory(),
                    'typeEstimated' => [
                        'beds'         => $accommodationType->getCapacity()->getMainPlaces(),
                        'bedType'      => 'DOUBLE',
                        'extraBeds'    => $accommodationType->getCapacity()->getExtraPlaces(),
                        'extraBedType' => 'NO_EXTRA_BED',
                    ],
                    'description' => $accommodationType->getMetadata()['description'],
                    'gallery'     => $this->transformGallery($accommodationType->getGallery()),
                    'thumbnail'   => $this->transformThumbnail($accommodationType->getThumbnail()),
                    'amenities'   => $accommodationType->getAmenities(),
                    'address'     => $accommodationType->getMetadata()['contactInfo']['address'] ?? '',
                    'longitude'   => $accommodationType->getMetadata()['contactInfo']['longitude'] ?? '',
                    'latitude'    => $accommodationType->getMetadata()['contactInfo']['latitude'] ?? '',
                ],
                'ratePlans'      => $preparedRatePlans,
                'additionalFees' => []
            ];
        }

        return $offers;
    }

    /**
     * Returns included extra services for a rate plan scoped to an accommodation type.
     * Override in Pro subclass to populate from Extras extension data.
     *
     * The caller guarantees that $accommodationTypeId belongs to the rate plan.
     *
     * @return array<int, array<string, mixed>>
     */
    protected function getExtrasForRatePlan(RatePlan $ratePlan, int $accommodationTypeId): array
    {
        return [];
    }

    private function prepareOffers(
        array $ratePlanGroup,
        array $ratePlans,
        DateTimeImmutable $checkInDateTime,
        int $accommodationTypeId
    ): array {
        if (empty($ratePlanGroup)) {
            return [];
        }

        $result = [];
        foreach ($ratePlanGroup as $ratePlanId => $variations) {
            if ($ratePlanId == null) {
                continue;
            }

            /** @var RatePlan $ratePlan */
            $ratePlan = $ratePlans[$ratePlanId];
            if ($ratePlan->isHiddenFromFrontend()) {
                continue;
            }

            $preparedVariations = $this->prepareVariations($variations);
            $paymentTypes = $this->getAvailablePaymentTypes($ratePlan->getPaymentTypes(), $checkInDateTime);

            if (empty($preparedVariations) || empty($paymentTypes)) {
                continue;
            }

            $cancellationPolicy = $ratePlan->getCancellationPolicy();

            $result[] = [
                'id'                 => $ratePlan->getId(),
                'rateCode'           => $ratePlan->getCode(),
                'name'               => $ratePlan->getName(),
                'boardType'          => $ratePlan->getBoardType(),
                'description'        => $ratePlan->getDescription(),
                'cancellationPolicy' => [
                    'name'  => $cancellationPolicy->getName(),
                    'rules' => $cancellationPolicy->getCancellationConsequences($checkInDateTime),
                ],
                'paymentTypes' => $paymentTypes,
                'extras'       => $this->getExtrasForRatePlan($ratePlan, $accommodationTypeId),
                'feed'         => [
                    'name' => $ratePlan->getBoardType(),
                ],
                'variations' => $preparedVariations,
            ];
        }

        return $result;
    }

    private function getAvailablePaymentTypes(array $paymentTypes, DateTimeImmutable $checkInDateTime): array
    {
        $paymentTypes = array_values(array_filter(
            $paymentTypes,
            fn (PaymentType $paymentType) => $paymentType->isAvailableOn($checkInDateTime)
        ));

        return array_map(fn (PaymentType $paymentType) => [
            'name'        => $paymentType->getName(),
            'description' => $paymentType->getDescription(),
        ], $paymentTypes);
    }

    private function transformThumbnail(array $thumbnail): ?array
    {
        if (empty($thumbnail)) {
            return null;
        }

        return [
            'url'   => $thumbnail['sizes']['card']['url'] ?? $thumbnail['url'] ?? '',
            'name'  => $thumbnail['name'] ?? '',
            'sizes' => $thumbnail['sizes'] ?? null,
        ];
    }

    private function transformGallery(array $gallery): array
    {
        return array_map(fn(array $img) => [
            'url'     => $img['sizes']['gallery']['url'] ?? $img['url'] ?? '',
            'name'    => $img['name'] ?? '',
            'caption' => $img['caption'] ?? '',
            'sizes'   => $img['sizes'] ?? null,
        ], $gallery);
    }

    private function prepareVariations(array $variations): array
    {
        $result = [];
        foreach ($variations as $variation) {
            /** @var StayCostCalculatorResult|null $stayCost */
            $stayCost = $variation['stayCost'];

            if ($variation['scenario'] === null) {
                continue;
            }

            // Children scenarios never have a stayCost — output them as a pricing placeholder.
            // They carry no concrete placement, so allocation is empty (kept for a uniform contract).
            if ($variation['scenario']['kind'] === 'children') {
                $result[] = [
                    'occupancyOptions' => [
                        'kind'     => $variation['scenario']['kind'],
                        'main'     => $variation['scenario']['main'],
                        'extraBed' => $variation['scenario']['extra'],
                    ],
                    'allocation' => [],
                    'price' => null,
                ];
                continue;
            }

            // filter out variations without stay cost
            if (
                $stayCost === null
                || $stayCost->hasError()
                || count($stayCost->getDailyCosts()) === 0
                || count($stayCost->getRestrictions()) > 0
            ) {
                continue;
            }

            $summary  = $stayCost->getCompleteSummary();
            if ($discountPercentage = $stayCost->getInheritanceDiscountPercentage()) {
                $discountPercentage = (int) round($discountPercentage);
            }

            $result[] = [
                'occupancyOptions' => [
                    'kind'     => $variation['scenario']['kind'],
                    'main'     => $variation['scenario']['main'],
                    'extraBed' => $variation['scenario']['extra'],
                ],
                'allocation' => $this->projectAllocation($variation['allocationScheme'] ?? null),
                'price' => [
                    'currency'             => $summary->total()->getCurrency(),
                    'sellingPrice'         => $summary->total()->getMinorAmount()->toInt(),
                    'originalSellingPrice' => $discountPercentage > 0
                        ? $summary->total()->dividedBy(1 - ($discountPercentage / 100), RoundingMode::HALF_UP)->getMinorAmount()->toInt()
                        : null,
                    'discount'             => $discountPercentage,
                    'details'              => [
                        'subtotal'  => $summary->subtotal()->getMinorAmount()->toInt(),
                        'taxes'     => $summary->totalTaxes()->getMinorAmount()->toInt(),
                        'charges'   => $summary->totalCharges()->getMinorAmount()->toInt(),
                        'discounts' => $summary->totalDiscounts()->getMinorAmount()->toInt(),
                        'total'     => $summary->total()->getMinorAmount()->toInt(),
                    ]
                ],
                'dailyBreakdown' => $this->buildDailyBreakdown($stayCost),
            ];
        }

        return $result;
    }

    /**
     * Project a concrete allocation scheme onto a people-level breakdown.
     *
     * Unlike occupancyOptions (physical beds), this describes who is placed where —
     * including "without place" guests and children's ages. It is a stay-wide fact
     * (a single scheme applies to the whole booking), so it is exposed once per variation.
     *
     * Returns an empty array for abstract offers that have no concrete allocation.
     *
     * @return array<int, array<string, mixed>>
     */
    private function projectAllocation(?AllocationScheme $allocationScheme): array
    {
        if ($allocationScheme === null) {
            return [];
        }

        return array_map(function (Allocation $allocation) {
            $guestType = $allocation->getGuestType();

            return [
                'guestType' => $guestType->getTypeKey(),
                'location'  => $allocation->getLocation()->toString(),
                'quantity'  => $allocation->getQuantity(),
                'variant'   => $guestType->getVariantKey(),
                'age'       => $guestType instanceof ChildGuest ? $guestType->getAge() : null,
            ];
        }, $allocationScheme->getAllocations());
    }

    /**
     * Build per-day placement breakdown for a stay cost.
     *
     * Each day exposes the placement line items used to compose the price
     * (which adults/children landed on which place + variant + price/total).
     *
     * @return array<int, array<string, mixed>>
     */
    private function buildDailyBreakdown(StayCostCalculatorResult $stayCost): array
    {
        return array_map(
            fn(StayCostByDate $daily) => [
                'date'       => $daily->getDateString(),
                'basePrice'  => $daily->getBasePrice()->getMinorAmount()->toInt(),
                'placements' => $daily->getPriceDetails(),
            ],
            $stayCost->getDailyCosts()
        );
    }
}
