<?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\Entity;

use Doctrine\ORM\Mapping as ORM;
use InPost\International\Api\Shipment\Request\ShipmentInterface;
use InPost\International\Api\Shipment\Response\Shipment as ApiShipment;
use InPost\International\Api\Shipment\ShipmentType;
use InPost\International\Api\Tracking\Model\Parcel as ApiParcel;
use InPost\International\Shipment\Exception\ShipmentException;
use InPost\International\Shipment\ShipmentRepository;
use InPost\International\Shipment\ShippingMethod;
use PrestaShop\PrestaShop\Core\Domain\Order\ValueObject\OrderId;

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

/**
 * @final
 *
 * @ORM\Entity(repositoryClass=ShipmentRepository::class)
 * @ORM\Table(name=Shipment::TABLE_NAME)
 * @ORM\HasLifecycleCallbacks
 */
class Shipment
{
    /**
     * @internal
     */
    public const TABLE_NAME = _DB_PREFIX_ . 'inpost_intl_shipment';

    /**
     * @var int|null
     *
     * @ORM\Id
     * @ORM\GeneratedValue
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @var int
     *
     * @ORM\Column(type="integer", name="id_order")
     */
    private $orderId;

    /**
     * @var bool
     *
     * @ORM\Column(type="boolean", name="is_sandbox")
     */
    private $sandbox;

    /**
     * @var string
     *
     * @ORM\Column(type="string")
     */
    private $type;

    /**
     * @var string
     *
     * @ORM\Column(type="guid")
     */
    private $uuid;

    /**
     * @var string
     *
     * @ORM\Column(type="string")
     */
    private $trackingNumber;

    /**
     * @var string
     *
     * @ORM\Column(type="string")
     */
    private $status;

    /**
     * @var array<string, string>|null
     *
     * @ORM\Column(type="json", name="reference", nullable=true)
     */
    private $references;

    /**
     * @var string
     *
     * @ORM\Column(type="string")
     */
    private $recipientEmail;

    /**
     * @var Phone
     *
     * @ORM\Embedded(class=Phone::class)
     */
    private $recipientPhone;

    /**
     * @var Parcel
     *
     * @ORM\Embedded(class=Parcel::class)
     */
    private $parcel;

    /**
     * @var Money|null
     *
     * @ORM\Embedded(class=Money::class)
     */
    private $insurance;

    /**
     * @var PickupAddress|null
     *
     * @ORM\ManyToOne(targetEntity=PickupAddress::class, inversedBy="shipments")
     */
    private $pickupAddress;

    /**
     * @var PickupOrder|null
     *
     * @ORM\ManyToOne(targetEntity=PickupOrder::class, inversedBy="shipments")
     */
    private $pickupOrder;

    /**
     * @var \DateTimeImmutable|null
     *
     * @ORM\Column(type="datetime_immutable")
     */
    private $createdAt;

    /**
     * @var \DateTimeImmutable|null
     *
     * @ORM\Column(type="datetime_immutable")
     */
    private $updatedAt;

    private function __construct(int $orderId, bool $sandbox, ShipmentType $type, string $uuid, string $trackingNumber, string $status, string $recipientEmail, Phone $recipientPhone, Parcel $parcel, Money $insurance = null, array $references = null, PickupAddress $pickupAddress = null)
    {
        $this->orderId = $orderId;
        $this->sandbox = $sandbox;
        $this->type = $type->value;
        $this->uuid = $uuid;
        $this->trackingNumber = $trackingNumber;
        $this->status = $status;
        $this->references = $references;
        $this->recipientEmail = $recipientEmail;
        $this->recipientPhone = $recipientPhone;
        $this->parcel = $parcel;
        $this->insurance = $insurance;
        $this->pickupAddress = $pickupAddress;
    }

    public static function fromPoint(OrderId $orderId, bool $sandbox, ShipmentInterface $request, ApiShipment $response): self
    {
        if (ShippingMethod::FromPoint() !== ShippingMethod::fromShipmentType($request->getType())) {
            throw new ShipmentException('Shipping method mismatch.');
        }

        return self::create($orderId, $sandbox, $request, $response);
    }

    public static function courierPickup(OrderId $orderId, bool $sandbox, ShipmentInterface $request, ApiShipment $response, PickupAddress $pickupAddress): self
    {
        if (ShippingMethod::CourierPickup() !== ShippingMethod::fromShipmentType($request->getType())) {
            throw new ShipmentException('Shipping method mismatch.');
        }

        return self::create($orderId, $sandbox, $request, $response, $pickupAddress);
    }

    public function getId(): ?int
    {
        return $this->id;
    }

    public function getOrderId(): int
    {
        return $this->orderId;
    }

    public function isSandbox(): bool
    {
        return $this->sandbox;
    }

    public function getType(): ShipmentType
    {
        return ShipmentType::from($this->type);
    }

    public function getUuid(): string
    {
        return $this->uuid;
    }

    public function getTrackingNumber(): string
    {
        return $this->trackingNumber;
    }

    public function getStatus(): string
    {
        return $this->status;
    }

    // TODO: cleanup
    //    public function updateStatus(ApiShipment $shipment): void
    //    {
    //        if ($this->uuid !== $shipment->getUuid()) {
    //            throw new ShipmentException('Shipment ID mismatch.');
    //        }
    //
    //        $this->status = $shipment->getStatus();
    //        $this->updatedAt = new \DateTimeImmutable();
    //    }

    public function updateStatus(ApiParcel $parcel): void
    {
        if ($this->trackingNumber !== $parcel->getTrackingNumber()) {
            throw new ShipmentException('Tracking number mismatch.');
        }

        $event = $parcel->getMostRecentEvent();

        if ($event->getTimestamp() <= $this->updatedAt) {
            return;
        }

        $this->status = $event->getCode();
        $this->updatedAt = $event->getTimestamp()->setTimezone(new \DateTimeZone(date_default_timezone_get()));
    }

    /**
     * @return array<string, string>
     */
    public function getReferences(): array
    {
        return $this->references ?? [];
    }

    public function getRecipientEmail(): string
    {
        return $this->recipientEmail;
    }

    public function getRecipientPhone(): Phone
    {
        return $this->recipientPhone;
    }

    public function getParcel(): Parcel
    {
        return $this->parcel;
    }

    public function getInsurance(): ?Money
    {
        return $this->insurance;
    }

    public function getPickupAddress(): ?PickupAddress
    {
        return $this->pickupAddress;
    }

    /**
     * @internal
     */
    public function setPickupOrder(PickupOrder $pickupOrder): void
    {
        if (null !== $this->pickupOrder) {
            throw new ShipmentException('Pickup was already ordered for this shipment.');
        }

        if (ShippingMethod::fromShipmentType($this->getType()) !== ShippingMethod::CourierPickup()) {
            throw new ShipmentException('Shipping method mismatch.');
        }

        $this->pickupOrder = $pickupOrder;
    }

    public function getCreatedAt(): ?\DateTimeImmutable
    {
        return $this->createdAt;
    }

    public function getUpdatedAt(): ?\DateTimeImmutable
    {
        return $this->updatedAt;
    }

    /**
     * @internal
     *
     * @ORM\PrePersist()
     */
    public function onPrePersist(): void
    {
        if (null !== $this->createdAt) {
            return;
        }

        $this->createdAt = $this->updatedAt = new \DateTimeImmutable();
    }

    // TODO: cleanup
    //    /**
    //     * @internal
    //     *
    //     * @ORM\PreUpdate()
    //     */
    //    public function onPreUpdate(): void
    //    {
    //        $this->updatedAt = new \DateTimeImmutable();
    //    }

    private static function create(OrderId $orderId, bool $sandbox, ShipmentInterface $request, ApiShipment $response, PickupAddress $pickupAddress = null): self
    {
        $recipientPhone = new Phone(
            $request->getRecipient()->getPhone()->getPrefix(),
            $request->getRecipient()->getPhone()->getNumber()
        );

        $parcel = self::createParcel($request);

        if (null !== $request->getReferences()) {
            $references = iterator_to_array($request->getReferences());
        } else {
            $references = null;
        }

        $insurance = self::mapInsurance($request);

        return new self(
            $orderId->getValue(),
            $sandbox,
            $request->getType(),
            $response->getUuid(),
            $response->getTrackingNumber(),
            $response->getStatus(),
            $request->getRecipient()->getEmail(),
            $recipientPhone,
            $parcel,
            $insurance,
            $references,
            $pickupAddress
        );
    }

    private static function createParcel(ShipmentInterface $request): Parcel
    {
        $dimensions = new Dimensions(
            $request->getParcel()->getDimensions()->getLength(),
            $request->getParcel()->getDimensions()->getWidth(),
            $request->getParcel()->getDimensions()->getHeight(),
            $request->getParcel()->getDimensions()->getUnit()
        );

        $weight = new Weight(
            $request->getParcel()->getWeight()->getAmount(),
            $request->getParcel()->getWeight()->getUnit()
        );

        if (null !== $parcelLabel = $request->getParcel()->getLabel()) {
            $comment = $parcelLabel->getComment();
        } else {
            $comment = null;
        }

        return new Parcel(
            $request->getParcel()->getType(),
            $dimensions,
            $weight,
            $comment
        );
    }

    private static function mapInsurance(ShipmentInterface $request): ?Money
    {
        if (null === $services = $request->getValueAddedServices()) {
            return null;
        }

        $insurance = $services->getInsurance();

        if (null === $insurance || 0. === $insurance->getValue()) {
            return null;
        }

        $currency = $insurance->getCurrency();

        return new Money(
            $currency->convertToSmallestUnit($insurance->getValue()),
            $currency->value
        );
    }
}
