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

use Doctrine\ORM\EntityManagerInterface;
use InPost\International\Api\ApiClientFactory;
use InPost\International\Api\ApiClientInterface;
use InPost\International\Api\Exception\ApiExceptionInterface;
use InPost\International\Api\Tracking\Response\TrackingDetailsResponse;
use InPost\International\Entity\Shipment;
use InPost\International\Environment\ProductionEnvironment;
use InPost\International\Shipment\Event\ShipmentStatusUpdatedEvent;
use InPost\International\Shipment\ShipmentRepository;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\StyleInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;

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

final class UpdateShipmentsCommand extends Command
{
    public const NAME = 'inpost:intl:update-shipments';

    protected static $defaultName = self::NAME;

    /**
     * @var EntityManagerInterface
     */
    private $manager;

    /**
     * @var ShipmentRepository
     */
    private $repository;

    /**
     * @var ApiClientFactory
     */
    private $clientFactory;

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

    /**
     * @var LoggerInterface
     */
    private $logger;

    /**
     * @var \Context
     */
    private $context;

    /**
     * @var StyleInterface
     */
    private $io;

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

    /**
     * @var int
     */
    private $processedShipments = 0;

    /**
     * @var int
     */
    private $updatedShipments = 0;

    public function __construct(EntityManagerInterface $manager, ApiClientFactory $clientFactory, EventDispatcherInterface $dispatcher, LoggerInterface $logger, \Context $context)
    {
        parent::__construct();

        /** @var ShipmentRepository $repository */
        $repository = $manager->getRepository(Shipment::class);

        $this->manager = $manager;
        $this->repository = $repository;
        $this->clientFactory = $clientFactory;
        $this->dispatcher = $dispatcher;
        $this->logger = $logger;
        $this->context = $context;
    }

    protected function configure(): void
    {
        $this
            ->setDescription('Updates InPost International shipments\' statuses.')
            ->addOption('environment', null, InputOption::VALUE_REQUIRED, sprintf('The API environment to use (defaults to "%s").', ProductionEnvironment::ID));
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $this->io = new SymfonyStyle($input, $output);
        $this->processedShipments = $this->updatedShipments = 0;

        $env = $input->getOption('environment') ?? ProductionEnvironment::ID;
        $this->client = $this->clientFactory->create($env);

        $sandbox = $this->repository->enableSandboxFilter($this->client->getEnvironment()->isTestEnvironment());

        /* @see \PrestaShop\PrestaShop\Core\Stock\StockManager::saveMovement() throws if kernel container is available but no context employee is set (called during order status update). */
        $employee = $this->context->employee;
        if (null === $employee || null === $employee->id) {
            $this->context->employee = $this->createEmployee();
        }

        try {
            if (!$this->updateShipments()) {
                return 1;
            }

            $this->io->success(sprintf('Updated %d shipments (total processed: %d).', $this->updatedShipments, $this->processedShipments));

            return 0;
        } catch (ApiExceptionInterface $e) {
            $this->io->error(sprintf('Could not retrieve API data for shipment update: %s', $e->getMessage()));

            return 1;
        } catch (\Throwable $e) {
            $this->logger->critical('An error occurred while updating shipments: {exception}', [
                'exception' => $e,
            ]);

            throw $e;
        } finally {
            // restore previous context and filters
            $this->context->employee = $employee;

            if (null === $sandbox) {
                $this->repository->disableSandboxFilter();
            } else {
                $this->repository->enableSandboxFilter($sandbox);
            }
        }
    }

    private function updateShipments(): bool
    {
        $result = true;
        $batch = [];

        foreach ($this->getShipments() as $shipment) {
            $batch[] = $shipment;

            if (10 === count($batch)) {
                $result = $result && $this->processBatch($batch);
                $batch = [];
            }
        }

        if ([] !== $batch) {
            $result = $result && $this->processBatch($batch);
        }

        return $result;
    }

    /**
     * @param Shipment[] $shipments
     */
    private function processBatch(array $shipments): bool
    {
        $result = true;

        $trackingNumbers = array_map(static function (Shipment $shipment): string {
            return $shipment->getTrackingNumber();
        }, $shipments);

        $trackingDetails = $this->client->getTrackingDetails(...$trackingNumbers);

        foreach ($shipments as $shipment) {
            try {
                $this->updateShipmentStatus($shipment, $trackingDetails);
                $this->manager->detach($shipment);
            } catch (\Throwable $e) {
                $this->logger->error('Could not update shipment #{shipmentId}: {exception}', [
                    'shipmentId' => $shipment->getId(),
                    'exception' => $e,
                ]);

                $this->io->error(sprintf('Could not update shipment #%d: %s', $shipment->getId(), $e->getMessage()));
                $result = false;
            } finally {
                ++$this->processedShipments;
            }
        }

        return $result;
    }

    private function updateShipmentStatus(Shipment $shipment, TrackingDetailsResponse $trackingDetails): void
    {
        if (null === $parcel = $trackingDetails->getParcel($shipment->getTrackingNumber())) {
            return;
        }

        $previousStatus = $shipment->getStatus();
        $shipment->updateStatus($parcel);

        if ($previousStatus === $shipment->getStatus()) {
            return;
        }

        $this->repository->add($shipment);
        $this->dispatcher->dispatch(new ShipmentStatusUpdatedEvent($shipment, $previousStatus));

        ++$this->updatedShipments;
    }

    /**
     * @return iterable<Shipment>
     */
    private function getShipments(): iterable
    {
        $query = $this->manager->createQueryBuilder()
            ->select('s')
            ->from(Shipment::class, 's')
            ->where('s.status NOT LIKE :status')
            ->setParameter('status', 'EOL.%')
            ->getQuery();

        if (!is_callable([$query, 'toIterable'])) {
            return $query->iterate();
        }

        return $query->toIterable();
    }

    private function createEmployee(): \Employee
    {
        $employee = new \Employee();
        $employee->id = 0;
        $employee->firstname = 'InPost';
        $employee->lastname = 'International';

        return $employee;
    }
}
