<?php
/**
 * Copyright since 2025 InPost S.A.
 *
 * For the full license information, please view the LICENSE file bundled with the module.
 *
 * @author InPost S.A.
 * @copyright since 2025 InPost S.A.
 * @license MIT
 */

declare(strict_types=1);

namespace InPost\International\Checkout\MessageHandler;

use InPost\International\Api\Point\Model\PointCapability;
use InPost\International\Checkout\CheckoutSessionRepositoryInterface;
use InPost\International\Checkout\Exception\CheckoutException;
use InPost\International\Checkout\Form\Model\PointDeliveryOptions;
use InPost\International\Checkout\Message\UpdateCheckoutSessionCommand;
use InPost\International\Country;
use InPost\International\Delivery\Point\PointDataProviderInterface;
use InPost\International\Delivery\Util\CarrierFinder;
use InPost\International\Entity\Carrier;
use InPost\International\Entity\CheckoutSession;
use InPost\International\Entity\PointDeliveryCarrier;
use InPost\International\PrestaShop\ObjectModel\Repository\ObjectRepositoryInterface;
use PrestaShop\PrestaShop\Core\Domain\Cart\Exception\CartNotFoundException;
use PrestaShop\PrestaShop\Core\Domain\Cart\ValueObject\CartId;

if (!defined('_PS_VERSION_')) {
    exit;
}

final class UpdateCheckoutSessionHandler
{
    /**
     * @var ObjectRepositoryInterface<\Cart>
     */
    private $cartRepository;

    /**
     * @var CarrierFinder
     */
    private $carrierFinder;

    /**
     * @var CheckoutSessionRepositoryInterface
     */
    private $sessionRepository;

    /**
     * @var PointDataProviderInterface
     */
    private $pointDataProvider;

    /**
     * @param ObjectRepositoryInterface<\Cart> $cartRepository
     */
    public function __construct(ObjectRepositoryInterface $cartRepository, CarrierFinder $carrierFinder, CheckoutSessionRepositoryInterface $sessionRepository, PointDataProviderInterface $pointDataProvider)
    {
        $this->cartRepository = $cartRepository;
        $this->carrierFinder = $carrierFinder;
        $this->sessionRepository = $sessionRepository;
        $this->pointDataProvider = $pointDataProvider;
    }

    public function __invoke(UpdateCheckoutSessionCommand $command): void
    {
        $cartId = $command->getCartId();

        if (null === $cart = $this->cartRepository->find($cartId)) {
            throw new CartNotFoundException(sprintf('Cart with ID "%d" does not exist.', $cartId));
        }

        if ([] === $carriers = $this->carrierFinder->findByDeliveryOption($cart->getDeliveryOption())) {
            throw new CheckoutException('The selected delivery option in not associated with an InPost carrier.');
        }

        $deliveryPoints = [];

        foreach ($carriers as $carrier) {
            $options = $command->getCarrierOptions($carrier->getReferenceId());

            if (null === $data = $this->processDeliveryOptions($carrier, $options)) {
                continue;
            }

            $deliveryPoints[] = [$carrier, $data['point']];
        }

        if ([] === $deliveryPoints) {
            return;
        }

        $this->updateSession($cartId, $deliveryPoints);
    }

    public function handle(UpdateCheckoutSessionCommand $command): void
    {
        ($this)($command);
    }

    private function updateSession(int $cartId, array $deliveryPoints): void
    {
        $session = $this->sessionRepository->find($cartId) ?? new CheckoutSession(new CartId($cartId));
        $session->resetDeliveryOptions();

        foreach ($deliveryPoints as [$carrier, $point]) {
            $session->selectDeliveryPoint($carrier, $point);
        }

        $this->sessionRepository->add($session);
    }

    private function processDeliveryOptions(Carrier $carrier, ?object $options): ?array
    {
        if ($carrier instanceof PointDeliveryCarrier) {
            return $this->processPointDeliveryOptions($carrier, $options);
        }

        if (null !== $options) {
            throw new CheckoutException(sprintf('Invalid delivery options for carrier ID "%d".', $carrier->getReferenceId()));
        }

        return null;
    }

    private function processPointDeliveryOptions(PointDeliveryCarrier $carrier, ?object $options): array
    {
        if (!$options instanceof PointDeliveryOptions) {
            throw new CheckoutException(sprintf('Invalid delivery options for carrier ID "%d".', $carrier->getReferenceId()));
        }

        $pointId = $options->getPointId();

        if (null === $pointId) {
            throw new CheckoutException('Point ID is required.');
        }

        try {
            $point = $this->pointDataProvider->getPoint($pointId);
        } catch (\Exception $e) {
            throw new CheckoutException('Could not fetch point data.', 0, $e);
        }

        if (null === $point) {
            throw new CheckoutException(sprintf('Point "%s" does not exist.', $pointId));
        }

        if (!$point->hasCapability(PointCapability::ParcelCollect())) {
            throw new CheckoutException(sprintf('Point "%s" does not support parcel collection.', $pointId));
        }

        $country = $point->getCountry();

        if (!in_array(Country::tryFrom($country), $carrier->getCountries(), true)) {
            throw new CheckoutException(sprintf('Invalid point country ("%s") for carrier ID "%d".', $country, $carrier->getReferenceId()));
        }

        return ['point' => $point];
    }
}
