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

use InPost\International\Api\Exception\ApiClientExceptionInterface;
use InPost\International\Api\Pickup\Model\Address;
use InPost\International\Api\Pickup\Model\ContactPerson;
use InPost\International\Api\Pickup\Model\ItemType;
use InPost\International\Api\Pickup\Model\Phone;
use InPost\International\Api\Pickup\Model\PickupTime;
use InPost\International\Api\Pickup\Model\References;
use InPost\International\Api\Pickup\Model\Volume;
use InPost\International\Api\Pickup\Model\Weight;
use InPost\International\Api\Pickup\Model\WeightUnit;
use InPost\International\Api\Pickup\PickupsApiClientInterface;
use InPost\International\Api\Pickup\Request\CreatePickupOrderRequest;
use InPost\International\Api\Shipment\Model\Parcel\WeightUnit as ShipmentWeightUnit;
use InPost\International\Entity\PickupAddress;
use InPost\International\Entity\PickupOrder;
use InPost\International\Entity\Shipment;
use InPost\International\PickupOrder\Event\PickupOrderCreatedEvent;
use InPost\International\PickupOrder\Exception\NoShipmentWithoutPickupOrderFoundException;
use InPost\International\PickupOrder\Exception\PickupOrderException;
use InPost\International\PickupOrder\Message\CreatePickupOrderCommand;
use InPost\International\PickupOrder\PickupOrderRepositoryInterface;
use InPost\International\Shipment\ShipmentRepositoryInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;

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

final class CreatePickupOrderHandler
{
    /**
     * @var ShipmentRepositoryInterface
     */
    private $shipmentRepository;

    /**
     * @var PickupsApiClientInterface
     */
    private $client;

    /**
     * @var PickupOrderRepositoryInterface
     */
    private $pickupOrderRepository;

    /**
     * @var EventDispatcherInterface
     */
    private $dispatcher;

    public function __construct(ShipmentRepositoryInterface $shipmentRepository, PickupsApiClientInterface $client, PickupOrderRepositoryInterface $pickupOrderRepository, EventDispatcherInterface $dispatcher)
    {
        $this->shipmentRepository = $shipmentRepository;
        $this->client = $client;
        $this->pickupOrderRepository = $pickupOrderRepository;
        $this->dispatcher = $dispatcher;
    }

    /**
     * @throws PickupOrderException
     * @throws ApiClientExceptionInterface
     */
    public function __invoke(CreatePickupOrderCommand $command): PickupOrder
    {
        $shipments = $this->getShipments($command);

        $request = $this->createRequest($command, $shipments);
        $response = $this->client->createPickupOrder($request);

        $pickupOrder = PickupOrder::create(
            $command->getPickupAddress(),
            $request,
            $response,
            ...$shipments
        );

        $this->pickupOrderRepository->add($pickupOrder);
        $this->dispatcher->dispatch(new PickupOrderCreatedEvent($pickupOrder));

        return $pickupOrder;
    }

    public function handle(CreatePickupOrderCommand $command): PickupOrder
    {
        return ($this)($command);
    }

    /**
     * @return Shipment[]
     */
    private function getShipments(CreatePickupOrderCommand $command): array
    {
        if ([] === $shipmentIds = $command->getShipmentIds()) {
            throw new PickupOrderException('At least one shipment ID is required.');
        }

        $shipments = $this->shipmentRepository->getShipmentsWithoutPickupOrder(...$shipmentIds);

        if ([] === $shipments) {
            throw new NoShipmentWithoutPickupOrderFoundException('No shipments without pickup order found.');
        }

        return $shipments;
    }

    /**
     * @param Shipment[] $shipments
     */
    private function createRequest(CreatePickupOrderCommand $command, array $shipments): CreatePickupOrderRequest
    {
        if (null === $pickupAddress = $command->getPickupAddress()) {
            throw new PickupOrderException('Pickup address is required.');
        }

        return new CreatePickupOrderRequest(
            $this->mapAddressData($pickupAddress),
            $this->mapContactPersonData($pickupAddress),
            $this->getPickupTime($command),
            $this->getVolume($shipments),
            $this->getReferences($command)
        );
    }

    private function mapAddressData(PickupAddress $pickupAddress): Address
    {
        $address = $pickupAddress->getAddress();

        return new Address(
            $address->getCountry(),
            $address->getCity(),
            $address->getPostcode(),
            $address->getStreet(),
            $address->getHouseNumber(),
            $address->getFlatNumber(),
            $address->getDescription()
        );
    }

    private function mapContactPersonData(PickupAddress $pickupAddress): ContactPerson
    {
        $contactPerson = $pickupAddress->getContactPerson();

        $phone = new Phone(
            $contactPerson->getPhone()->getPrefix(),
            $contactPerson->getPhone()->getNumber()
        );

        return new ContactPerson(
            $contactPerson->getFirstName(),
            $contactPerson->getLastName(),
            $contactPerson->getEmail(),
            $phone
        );
    }

    private function getPickupTime(CreatePickupOrderCommand $command): PickupTime
    {
        if (null === $date = $command->getPickupDate()) {
            throw new PickupOrderException('Pickup date is required.');
        }

        $time = $command->getPickupTime();

        if (null === $time || null === $time->getFrom() || null === $time->getTo()) {
            throw new PickupOrderException('Pickup time is required.');
        }

        $from = $date->setTime((int) $time->getFrom()->format('H'), (int) $time->getFrom()->format('i'));
        $to = $date->setTime((int) $time->getTo()->format('H'), (int) $time->getTo()->format('i'));

        return new PickupTime($from, $to);
    }

    /**
     * @param Shipment[] $shipments
     */
    private function getVolume(array $shipments): Volume
    {
        $weightKg = array_sum(array_map(static function (Shipment $shipment): float {
            return $shipment->getParcel()->getWeight()->convert(ShipmentWeightUnit::Kilogram());
        }, $shipments));

        $weight = new Weight(
            round($weightKg, 3, PHP_ROUND_HALF_UP),
            WeightUnit::Kilogram()
        );

        return new Volume(ItemType::Parcel(), count($shipments), $weight);
    }

    private function getReferences(CreatePickupOrderCommand $command): ?References
    {
        $references = [];

        foreach ($command->getReferences() as $reference) {
            $key = (string) $reference->getKey();
            $references[$key] = (string) $reference->getValue();
        }

        if ([] === $references) {
            return null;
        }

        return new References($references);
    }
}
