<?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\Carrier\MessageHandler;

use InPost\International\Carrier\CarrierRepositoryInterface;
use InPost\International\Carrier\Exception\CarrierException;
use InPost\International\Carrier\Exception\CarrierNotFoundException;
use InPost\International\Carrier\Message\EditCarrierCommand;
use InPost\International\Country;
use InPost\International\Entity\Carrier;
use InPost\International\PrestaShop\ObjectModel\Repository\CarrierRepository as ObjectModelRepository;
use InPost\International\PrestaShop\ObjectModel\Repository\CountryRepository;
use InPost\International\PrestaShop\ObjectModel\Repository\ObjectRepositoryInterface;

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

/**
 * @experimental constructor parameters may change
 */
final class EditCarrierHandler
{
    /**
     * @var CarrierRepositoryInterface
     */
    private $repository;

    /**
     * @var ObjectModelRepository
     */
    private $objectModelRepository;

    /**
     * @var CountryRepository
     */
    private $countryRepository;

    /**
     * @param ObjectModelRepository $objectModelRepository
     * @param CountryRepository $countryRepository
     */
    public function __construct(CarrierRepositoryInterface $repository, ObjectRepositoryInterface $objectModelRepository, ObjectRepositoryInterface $countryRepository)
    {
        $this->repository = $repository;
        $this->objectModelRepository = $objectModelRepository;
        $this->countryRepository = $countryRepository;
    }

    public function __invoke(EditCarrierCommand $command): void
    {
        $carrierId = $command->getCarrierId();

        if (null === $carrier = $this->repository->find($carrierId)) {
            throw new CarrierNotFoundException('Carrier does not exist.');
        }

        $model = $this->objectModelRepository->findOneByReferenceId($carrierId);

        if (null === $model) {
            throw new \RuntimeException(sprintf('Related native carrier does not exist for carrier #%d.', $carrierId));
        }

        if ($model->deleted) {
            throw new CarrierException('Cannot edit carrier that has been deleted.');
        }

        if (null !== $countries = $command->getCountries()) {
            $this->updateCountries($carrier, $model, $countries);
        }

        $this->repository->add($carrier);
    }

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

    /**
     * @param Country[] $countries
     */
    private function updateCountries(Carrier $carrier, \Carrier $model, array $countries): void
    {
        $carrier->setCountries(...$countries);

        if (false === $currentZones = $model->getZones()) {
            throw new \RuntimeException('Failed to retrieve current carrier zones data.');
        }

        $toAdd = [];
        $currentZoneIds = $toDelete = array_map(static function (array $zone): int {
            return (int) $zone['id_zone'];
        }, $currentZones);

        foreach ($countries as $country) {
            if (null === $zoneId = $this->getZoneIdByCountryCode($country->value)) {
                continue;
            }

            if (in_array($zoneId, $toAdd, true)) {
                continue;
            }

            if (false !== $key = array_search($zoneId, $currentZoneIds, true)) {
                unset($toDelete[$key]);

                continue;
            }

            $toAdd[] = $zoneId;
        }

        if ([] === $toAdd && [] === $toDelete) {
            return;
        }

        $model = $this->objectModelRepository->duplicateIfUsed($model);

        foreach ($toAdd as $zoneId) {
            if (!$model->addZone($zoneId)) {
                throw new \RuntimeException(sprintf('Failed to associate carrier #%d with zone #%d.', $model->id, $zoneId));
            }
        }

        foreach ($toDelete as $zoneId) {
            if (!$model->deleteZone($zoneId)) {
                throw new \RuntimeException(sprintf('Failed to remove association with zone #%d from carrier #%d.', $zoneId, $model->id));
            }
        }
    }

    private function getZoneIdByCountryCode(string $isoCode): ?int
    {
        if (null === $country = $this->countryRepository->findOneByIsoCode($isoCode)) {
            return null;
        }

        if (0 >= $zoneId = (int) $country->id_zone) {
            return null;
        }

        return $zoneId;
    }
}
