<?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\Controller\Admin;

use InPost\International\Api\Exception\ApiClientExceptionInterface;
use InPost\International\Api\Exception\ApiExceptionInterface;
use InPost\International\Api\Exception\ApiProblemException;
use InPost\International\Api\Exception\ValidationFailedException;
use InPost\International\Configuration\Repository\ApiConfigurationRepositoryInterface;
use InPost\International\Controller\AbstractController;
use InPost\International\Entity\Shipment;
use InPost\International\Http\Exception\HttpExceptionInterface;
use InPost\International\HttpFoundation\RequestHelper;
use InPost\International\PrestaShop\ObjectModel\Repository\ObjectRepositoryInterface;
use InPost\International\Shipment\Exception\NoOrdersWithoutShipmentFoundException;
use InPost\International\Shipment\Exception\ShipmentException;
use InPost\International\Shipment\Form\Type\ShipmentType;
use InPost\International\Shipment\Grid\ShipmentFilters;
use InPost\International\Shipment\Grid\ShipmentGridDefinitionFactory;
use InPost\International\Shipment\Message\BulkCreateShipmentsCommand;
use InPost\International\Shipment\Message\CreateShipmentCommand;
use InPost\International\Shipment\MessageHandler\Result\CreateShipmentsResult;
use InPost\International\Shipment\Order\CreateShipmentCommandBuilderInterface;
use InPost\International\Shipment\ShipmentRepositoryInterface;
use PrestaShop\PrestaShop\Core\Grid\Definition\Factory\OrderGridDefinitionFactory;
use PrestaShop\PrestaShop\Core\Grid\GridFactoryInterface;
use PrestaShop\PrestaShop\Core\Grid\Presenter\GridPresenterInterface;
use PrestaShopBundle\Security\Voter\PageVoter;
use Psr\Http\Client\NetworkExceptionInterface;
use Symfony\Component\Form\FormError;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\Routing\Annotation\Route;

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

/**
 * @Route(path="orders", name="admin_inpost_intl_orders_")
 */
final class OrderController extends AbstractController
{
    /**
     * @Route(path="/shipments", name="shipments", methods={"GET"})
     */
    public function shipments(Request $request, ApiConfigurationRepositoryInterface $configuration, GridFactoryInterface $gridFactory, GridPresenterInterface $gridPresenter): Response
    {
        $this->denyAccessUnlessGranted(PageVoter::READ, ShipmentController::TAB_NAME);

        if (!$request->query->has('orderIds') || !$orderIds = $request->query->get('orderIds')) {
            throw new UnprocessableEntityHttpException('"orderIds" parameter is required.');
        }

        $orderIds = array_map('intval', explode(',', $orderIds));

        $filters = ShipmentFilters::buildDefaults();
        $filters->addFilter([
            'id_order' => $orderIds,
            'is_sandbox' => $configuration->getConfiguration()->getEnvironment()->isTestEnvironment(),
        ]);
        $filters->set('limit', null);

        $grid = $gridFactory->getGrid($filters);

        if (in_array('application/json', $request->getAcceptableContentTypes(), true)) {
            return new JsonResponse([
                'items' => $grid->getData()->getRecords()->all(),
                'total' => $grid->getData()->getRecordsTotal(),
            ]);
        }

        return $this->render('@Modules/inpostinternational/views/templates/admin/components/_simple_grid.html.twig', [
            'grid' => $gridPresenter->present($grid),
        ]);
    }

    /**
     * @param ObjectRepositoryInterface<\Order> $orderRepository
     *
     * @Route(path="/{orderId}/create-shipment", name="create_shipment", methods={"GET", "POST"}, requirements={"orderId"="\d+"})
     */
    public function createShipment(int $orderId, Request $request, ObjectRepositoryInterface $orderRepository, CreateShipmentCommandBuilderInterface $commandBuilder): Response
    {
        $this->denyAccessUnlessGranted(PageVoter::CREATE, ShipmentController::TAB_NAME);

        if (null === $order = $orderRepository->find($orderId)) {
            return new Response('Order does not exist.', Response::HTTP_NOT_FOUND);
        }

        $commandBuilder->buildCommand($command = new CreateShipmentCommand(), $order);
        $command->setOrderId((int) $order->id);

        $form = $this->container->get('form.factory')->createNamed('inpost_intl_shipment', ShipmentType::class, $command, [
            'action' => $this->generateUrl('admin_inpost_intl_orders_create_shipment', [
                'orderId' => $orderId,
            ]),
        ]);
        $form->handleRequest($request);

        if ($form->isSubmitted() && $form->isValid()) {
            try {
                /** @var Shipment $shipment */
                $shipment = $this->handle($form->getData());

                if ($form->get('submitAndPrintLabel')->isClicked()) {
                    $response = $this->forward('InPost\International\Controller\Admin\ShipmentController::printLabel', [
                        'id' => $shipment->getId(),
                        '_legacy_controller' => ShipmentController::TAB_NAME,
                    ]);

                    if ($response->isSuccessful()) {
                        return $response;
                    }

                    $this->addFlash('error', $this->trans('Could not print shipment label.', [], 'Modules.Inpostinternational.Errors'));
                }

                if ($request->isXmlHttpRequest()) {
                    return new Response(null, Response::HTTP_CREATED);
                }

                $this->addFlash('success', $this->trans('Successful creation', [], 'Admin.Notifications.Success'));

                return $this->redirectToRoute('admin_inpost_intl_shipments_index', [], Response::HTTP_SEE_OTHER);
            } catch (ValidationFailedException $e) {
                $this->mapApiValidationErrors($form, $e);
            } catch (\Throwable $e) {
                $message = $this->getErrorMessageForException($e);

                if ($request->isXmlHttpRequest()) {
                    return new JsonResponse([
                        'message' => $message,
                    ], $this->getStatusCodeForException($e));
                }

                $this->addFlash('error', $message);
            }
        }

        $template = $request->isXmlHttpRequest()
            ? '@Modules/inpostinternational/views/templates/admin/shipment/_form.html.twig'
            : '@Modules/inpostinternational/views/templates/admin/shipment/create.html.twig';

        return $this->render($template, [
            'form' => $form->createView(),
            'orderId' => $orderId,
            'layoutTitle' => $this->trans('New shipment', [], 'Modules.Inpostinternational.Order'),
        ], $this->createResponseForForm($form));
    }

    /**
     * @Route(path="/create-shipments", name="bulk_create_shipments", methods={"POST"})
     */
    public function bulkCreateShipments(Request $request): Response
    {
        $this->denyAccessUnlessGranted(PageVoter::CREATE, ShipmentController::TAB_NAME);

        $orderIds = $this->getOrderIds($request);

        try {
            /** @var CreateShipmentsResult $result */
            $result = $this->handle(new BulkCreateShipmentsCommand(...$orderIds));
        } catch (NoOrdersWithoutShipmentFoundException $e) {
            $this->addFlash('warning', $this->trans('No orders without shipment found.', [], 'Modules.Inpostinternational.Errors'));

            return $this->redirectToRoute('admin_orders_index', [], Response::HTTP_SEE_OTHER);
        } catch (ShipmentException $e) {
            $this->addFlash('error', $e->getMessage());

            return $this->redirectToRoute('admin_orders_index', [], Response::HTTP_SEE_OTHER);
        }

        if ([] === $errors = $result->getErrors()) {
            $this->addFlash('success', $this->trans('Successful creation', [], 'Admin.Notifications.Success'));
        } else {
            foreach ($errors as $orderId => $e) {
                $this->addFlash('error', $this->trans('Could not create shipment for order #{orderId}: {error}', [
                    '{orderId}' => $orderId,
                    '{error}' => $this->getErrorMessageForException($e),
                ], 'Modules.Inpostinternational.Errors'));
            }
        }

        if (!$request->query->getBoolean('print_labels') || [] === $shipments = $result->getShipments()) {
            return $this->redirectToRoute('admin_orders_index', [], Response::HTTP_SEE_OTHER);
        }

        return $this->printLabels($request, $shipments);
    }

    /**
     * @Route(path="/print-shipment-labels", name="bulk_print_labels", methods={"POST"})
     */
    public function bulkPrintLabels(Request $request, ShipmentRepositoryInterface $repository): Response
    {
        $this->denyAccessUnlessGranted(PageVoter::READ, ShipmentController::TAB_NAME);

        $orderIds = $this->getOrderIds($request);

        if ([] === $shipments = $repository->findBy(['orderId' => $orderIds])) {
            $this->addFlash('warning', $this->trans('No shipments found for the selected orders.', [], 'Modules.Inpostinternational.Errors'));

            return $this->redirectToRoute('admin_orders_index', [], Response::HTTP_SEE_OTHER);
        }

        return $this->printLabels($request, $shipments);
    }

    /**
     * @Route(path="/update-shipment-statuses", name="bulk_update_status", methods={"POST"})
     */
    public function bulkUpdateStatus(Request $request, ShipmentRepositoryInterface $repository): Response
    {
        $this->denyAccessUnlessGranted(PageVoter::READ, ShipmentController::TAB_NAME);

        $orderIds = $this->getOrderIds($request);

        if ([] === $shipments = $repository->findBy(['orderId' => $orderIds])) {
            $this->addFlash('warning', $this->trans('No shipments found for the selected orders.', [], 'Modules.Inpostinternational.Errors'));

            return $this->redirectToRoute('admin_orders_index', [], Response::HTTP_SEE_OTHER);
        }

        $this->forwardBulkShipmentsAction($request, $shipments, 'InPost\International\Controller\Admin\ShipmentController::bulkUpdateStatus');

        return $this->redirectToRoute('admin_orders_index', [], Response::HTTP_SEE_OTHER);
    }

    private function mapApiValidationErrors(FormInterface $form, ValidationFailedException $e): void
    {
        foreach ($e->getErrors() as $path => $errors) {
            $path = preg_replace('/^shipment\./', '', $path);
            if (!is_array($errors)) {
                $errors = [$errors];
            }
            $this->addFormErrors($form, $path, array_map([$this, 'getApiErrorMessage'], $errors));
        }
    }

    private function addFormErrors(FormInterface $form, string $path, array $messages): void
    {
        $originalPath = $path;

        while (str_contains($path, '.')) {
            [$name, $path] = explode('.', $path, 2);

            if ($form->has($name)) {
                $this->addFormErrors($form->get($name), $path, $messages);

                return;
            }
        }

        if ($form->has($path)) {
            $form = $form->get($path);
            foreach ($messages as $message) {
                $form->addError(new FormError($message));
            }
        } else {
            foreach ($messages as $message) {
                $message = sprintf('%s: %s', $originalPath, $message);
                $form->addError(new FormError($message));
            }
        }
    }

    private function getApiErrorMessage(string $error): string
    {
        switch ($error) {
            case 'required':
                return $this->trans('This field is required', [], 'Admin.Notifications.Error');
            case 'invalid':
            case 'unknown':
            case 'not_found':
                return $this->trans('This value is not valid.', [], 'validators');
            case 'too_big':
                return $this->trans('This value is too big.', [], 'Modules.Inpostinternational.Validators');
            case 'too_small':
                return $this->trans('This value is too small.', [], 'Modules.Inpostinternational.Validators');
            case 'unavailable':
                return $this->trans('This service is not available for the selected destination country.', [], 'Modules.Inpostinternational.Validators');
            default:
                return $error;
        }
    }

    private function getOrderIds(Request $request): array
    {
        $orderIds = RequestHelper::getAll($request->request, OrderGridDefinitionFactory::GRID_ID . '_orders_bulk');

        if ([] === $orderIds) {
            throw new BadRequestHttpException('Order IDs list is empty.');
        }

        return array_map('intval', $orderIds);
    }

    /**
     * @param Shipment[] $shipments
     */
    private function printLabels(Request $request, array $shipments): Response
    {
        return $this->forwardBulkShipmentsAction($request, $shipments, 'InPost\International\Controller\Admin\ShipmentController::bulkPrintLabels');
    }

    private function forwardBulkShipmentsAction(Request $request, array $shipments, string $controller): Response
    {
        $shipmentIds = array_map(static function (Shipment $shipment) {
            return $shipment->getId();
        }, $shipments);

        $subRequest = $request->duplicate(null, [ShipmentGridDefinitionFactory::BULK_IDS_INPUT_NAME => $shipmentIds], [
            '_controller' => $controller,
            '_legacy_controller' => ShipmentController::TAB_NAME,
        ]);

        return $this->container->get('http_kernel')->handle($subRequest, HttpKernelInterface::SUB_REQUEST);
    }

    private function getErrorMessageForException(\Throwable $e): string
    {
        switch (true) {
            case $e instanceof ApiProblemException:
                return $e->getProblem()->getDetail() ?? $e->getMessage();
            case $e instanceof ApiExceptionInterface:
                return $this->trans('InPost API error: {message}', [
                    '{message}' => $e->getMessage(),
                ], 'Modules.Inpostinternational.Errors');
            case $e instanceof HttpExceptionInterface:
                return $this->trans('Unsuccessful InPost API response. Status code: {status}.', [
                    '{status}' => $e->getResponse()->getStatusCode(),
                ], 'Modules.Inpostinternational.Errors');
            case $e instanceof NetworkExceptionInterface:
                return $this->trans('Could not connect to the InPost API.', [], 'Modules.Inpostinternational.Errors');
            case $e instanceof ShipmentException:
            case $e instanceof ApiClientExceptionInterface:
                return $e->getMessage();
            default:
                $this->getLogger()->critical('An error occurred while creating shipment: {exception}', [
                    'exception' => $e,
                ]);

                if ($this->debug) {
                    throw $e;
                }

                return $this->trans('An unexpected error occurred.', [], 'Modules.Inpostinternational.Errors');
        }
    }

    private function getStatusCodeForException(\Throwable $e): int
    {
        switch (true) {
            case $e instanceof ShipmentException:
                return Response::HTTP_CONFLICT;
            case $e instanceof ApiClientExceptionInterface:
                return Response::HTTP_BAD_GATEWAY;
            default:
                return Response::HTTP_INTERNAL_SERVER_ERROR;
        }
    }
}
