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

use InPost\International\Api\ApiClientInterface;
use InPost\International\Api\Exception\ApiClientExceptionInterface;
use InPost\International\Api\Shipment\Model\Address;
use InPost\International\Api\Shipment\Model\Destination\DestinationAddress;
use InPost\International\Api\Shipment\Model\Destination\DestinationPoint;
use InPost\International\Api\Shipment\Model\Origin\OriginAddress;
use InPost\International\Api\Shipment\Model\Origin\OriginPoint;
use InPost\International\Api\Shipment\Model\Origin\PointShippingMethod;
use InPost\International\Api\Shipment\Model\Parcel;
use InPost\International\Api\Shipment\Model\Parcel\Dimensions;
use InPost\International\Api\Shipment\Model\Parcel\DimensionUnit;
use InPost\International\Api\Shipment\Model\Parcel\ParcelLabel;
use InPost\International\Api\Shipment\Model\Parcel\ParcelType;
use InPost\International\Api\Shipment\Model\Parcel\Weight;
use InPost\International\Api\Shipment\Model\Parcel\WeightUnit;
use InPost\International\Api\Shipment\Model\Phone;
use InPost\International\Api\Shipment\Model\Recipient;
use InPost\International\Api\Shipment\Model\References;
use InPost\International\Api\Shipment\Model\Sender;
use InPost\International\Api\Shipment\Model\Service\Currency;
use InPost\International\Api\Shipment\Model\Service\Insurance;
use InPost\International\Api\Shipment\Model\ShipmentPriority;
use InPost\International\Api\Shipment\Model\ValueAddedServices;
use InPost\International\Api\Shipment\Request\AddressToAddressShipment;
use InPost\International\Api\Shipment\Request\AddressToPointShipment;
use InPost\International\Api\Shipment\Request\CreateShipmentRequest;
use InPost\International\Api\Shipment\Request\LabelFormat;
use InPost\International\Api\Shipment\Request\PointToAddressShipment;
use InPost\International\Api\Shipment\Request\PointToPointShipment;
use InPost\International\Api\Shipment\Request\ShipmentInterface;
use InPost\International\Api\Shipment\ShipmentType;
use InPost\International\Carrier\CarrierType;
use InPost\International\Common\DTO\ContactDetails;
use InPost\International\Configuration\Repository\ShippingConfigurationRepositoryInterface;
use InPost\International\Country;
use InPost\International\Entity\Shipment;
use InPost\International\PrestaShop\ObjectModel\Repository\ObjectRepositoryInterface;
use InPost\International\Shipment\DTO\Parcel as ParcelDto;
use InPost\International\Shipment\Event\ShipmentCreatedEvent;
use InPost\International\Shipment\Exception\ParcelException;
use InPost\International\Shipment\Exception\ShipmentException;
use InPost\International\Shipment\Message\CreateShipmentCommand;
use InPost\International\Shipment\ShipmentRepositoryInterface;
use InPost\International\Shipment\ShippingMethod;
use PrestaShop\PrestaShop\Core\Domain\Order\Exception\OrderException;
use PrestaShop\PrestaShop\Core\Domain\Order\Exception\OrderNotFoundException;
use PrestaShop\PrestaShop\Core\Domain\Order\ValueObject\OrderId;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;

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

final class CreateShipmentHandler
{
    /**
     * @var ObjectRepositoryInterface<\Order>
     */
    private $orderRepository;

    /**
     * @var ShippingConfigurationRepositoryInterface
     */
    private $configurationRepository;

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

    /**
     * @var ShipmentRepositoryInterface
     */
    private $shipmentRepository;

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

    /**
     * @param ObjectRepositoryInterface<\Order> $orderRepository
     */
    public function __construct(ObjectRepositoryInterface $orderRepository, ShippingConfigurationRepositoryInterface $configurationRepository, ApiClientInterface $client, ShipmentRepositoryInterface $shipmentRepository, EventDispatcherInterface $dispatcher)
    {
        $this->orderRepository = $orderRepository;
        $this->configurationRepository = $configurationRepository;
        $this->client = $client;
        $this->shipmentRepository = $shipmentRepository;
        $this->dispatcher = $dispatcher;
    }

    /**
     * @throws OrderException
     * @throws ShipmentException
     * @throws ApiClientExceptionInterface
     */
    public function __invoke(CreateShipmentCommand $command): Shipment
    {
        $orderId = new OrderId($command->getOrderId());

        if (null === $this->orderRepository->find($orderId->getValue())) {
            throw new OrderNotFoundException($orderId, 'Order does not exist.');
        }

        $request = $this->createShipmentRequest($command);
        $response = $this->client->createShipment(new CreateShipmentRequest(LabelFormat::Pdf(), $request));

        if (ShippingMethod::CourierPickup() === $command->getShippingMethod()) {
            $shipment = Shipment::courierPickup($orderId, $this->isSandboxEnvironment(), $request, $response, $command->getPickupAddress());
        } else {
            $shipment = Shipment::fromPoint($orderId, $this->isSandboxEnvironment(), $request, $response);
        }

        $this->shipmentRepository->add($shipment);
        $this->dispatcher->dispatch(new ShipmentCreatedEvent($shipment));

        return $shipment;
    }

    public function handle(CreateShipmentCommand $command): Shipment
    {
        return ($this)($command);
    }

    private function createShipmentRequest(CreateShipmentCommand $command): ShipmentInterface
    {
        $shipmentType = $this->resolveShipmentType($command);
        $shipmentClass = $this->getShipmentRequestClass($shipmentType);

        $origin = ShippingMethod::CourierPickup() === $command->getShippingMethod()
            ? $this->getOriginAddress($command)
            : $this->getOriginPoint();

        $destination = CarrierType::Courier() === $command->getService()
            ? $this->getDestinationAddress($command)
            : $this->getDestinationPoint($command);

        return new $shipmentClass(
            $this->getSender(),
            $this->getRecipient($command),
            $origin,
            $destination,
            ShipmentPriority::Standard(),
            $this->getParcel($command),
            $this->getValueAddedServices($command),
            $this->getReferences($command)
        );
    }

    private function isSandboxEnvironment(): bool
    {
        return $this->client->getEnvironment()->isTestEnvironment();
    }

    private function resolveShipmentType(CreateShipmentCommand $command): ShipmentType
    {
        if (ShippingMethod::CourierPickup() === $command->getShippingMethod()) {
            return CarrierType::Courier() === $command->getService()
                ? ShipmentType::AddressToAddress()
                : ShipmentType::AddressToPoint();
        }

        if (CarrierType::Courier() === $command->getService()) {
            return ShipmentType::PointToAddress();
        }

        return ShipmentType::PointToPoint();
    }

    /**
     * @return class-string<ShipmentInterface>
     */
    private function getShipmentRequestClass(ShipmentType $shipmentType): string
    {
        switch ($shipmentType) {
            case ShipmentType::PointToPoint():
                return PointToPointShipment::class;
            case ShipmentType::AddressToPoint():
                return AddressToPointShipment::class;
            case ShipmentType::PointToAddress():
                return PointToAddressShipment::class;
            case ShipmentType::AddressToAddress():
                return AddressToAddressShipment::class;
            default:
                throw new \LogicException('Not implemented.');
        }
    }

    private function getSender(): Sender
    {
        if (null === $configuration = $this->configurationRepository->getShippingConfiguration()) {
            throw new ShipmentException('Sender details are not configured.');
        }

        return $configuration->getSender();
    }

    private function getRecipient(CreateShipmentCommand $command): Recipient
    {
        if (null === $recipient = $command->getRecipient()) {
            throw new ShipmentException('Recipient details are required.');
        }

        $phone = $this->getRecipientPhone($recipient);

        return new Recipient(
            (string) $recipient->getFirstName(),
            (string) $recipient->getLastName(),
            (string) $recipient->getEmail(),
            $phone,
            $recipient->getCompanyName()
        );
    }

    private function getRecipientPhone(ContactDetails $recipient): Phone
    {
        if (null === $phone = $recipient->getPhone()) {
            throw new ShipmentException('Recipient phone number is required.');
        }

        return new Phone(
            (string) $phone->getPrefix(),
            (string) $phone->getNumber()
        );
    }

    private function getOriginPoint(): OriginPoint
    {
        return new OriginPoint(Country::Poland(), PointShippingMethod::cases());
    }

    private function getOriginAddress(CreateShipmentCommand $command): OriginAddress
    {
        if (null === $command->getPickupAddress()) {
            throw new ShipmentException('Pickup address is required.');
        }

        $address = $command->getPickupAddress()->getAddress();

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

    private function getDestinationPoint(CreateShipmentCommand $command): DestinationPoint
    {
        if (null === $pointId = $command->getDestinationPointId()) {
            throw new ShipmentException('Destination point is required.');
        }

        try {
            [$countryCode] = explode('_', $pointId);

            return new DestinationPoint(Country::from($countryCode), $pointId);
        } catch (\Throwable $e) {
            throw new ShipmentException('Invalid destination point ID.');
        }
    }

    private function getDestinationAddress(CreateShipmentCommand $command): DestinationAddress
    {
        if (null === $address = $command->getDestinationAddress()) {
            throw new ShipmentException('Destination address is required.');
        }

        if (null === $country = $address->getCountryCode()) {
            throw new ShipmentException('Destination address country is required.');
        }

        return new DestinationAddress(new Address(
            (string) $address->getStreet(),
            (string) $address->getHouseNumber(),
            (string) $address->getCity(),
            (string) $address->getPostalCode(),
            $country,
            $address->getFlatNumber()
        ));
    }

    private function getParcel(CreateShipmentCommand $command): Parcel
    {
        if (null === $parcel = $command->getParcel()) {
            throw new ShipmentException('Parcel details are required.');
        }

        $type = $parcel->getType() ?? ParcelType::Standard();
        $dimensions = $this->getParcelDimensions($parcel);
        $weight = $this->getParcelWeight($parcel);
        $label = $this->getParcelLabel($parcel);

        return new Parcel($type, $dimensions, $weight, $label);
    }

    private function getParcelDimensions(ParcelDto $parcel): Dimensions
    {
        if (null === $dimensions = $parcel->getDimensions()) {
            throw new ParcelException('Parcel dimensions are required.');
        }

        return new Dimensions(
            (float) $dimensions->getLength(),
            (float) $dimensions->getWidth(),
            (float) $dimensions->getHeight(),
            $dimensions->getUnit() ?? DimensionUnit::getDefault()
        );
    }

    private function getParcelWeight(ParcelDto $parcel): Weight
    {
        if (null === $weight = $parcel->getWeight()) {
            throw new ParcelException('Parcel weight is required.');
        }

        return new Weight((float) $weight->getAmount(), $weight->getUnit() ?? WeightUnit::getDefault());
    }

    private function getParcelLabel(ParcelDto $parcel): ?ParcelLabel
    {
        if ($barcode = $parcel->getBarcode()) {
            return ParcelLabel::barcode($barcode);
        }

        if ($comment = $parcel->getComment()) {
            return ParcelLabel::comment($comment);
        }

        return null;
    }

    private function getValueAddedServices(CreateShipmentCommand $command): ?ValueAddedServices
    {
        if (0. === $insurance = (float) $command->getInsurance()) {
            return null;
        }

        return new ValueAddedServices(new Insurance($insurance, Currency::getDefault()));
    }

    private function getReferences(CreateShipmentCommand $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);
    }
}
