<?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\Tracking\Model\Parcel;
use InPost\International\Api\Tracking\TrackingEvent;
use InPost\International\Configuration\Repository\ApiConfigurationRepositoryInterface;
use InPost\International\Controller\AbstractController;
use InPost\International\File\StreamableInterface;
use InPost\International\HttpFoundation\RequestHelper;
use InPost\International\PickupOrder\Form\CutoffTimeRequestType;
use InPost\International\Shipment\Exception\NoShipmentsFoundException;
use InPost\International\Shipment\Exception\ShipmentNotFoundException;
use InPost\International\Shipment\Grid\ShipmentFilters;
use InPost\International\Shipment\Grid\ShipmentGridDefinitionFactory;
use InPost\International\Shipment\Message\BulkPrintLabelsCommand;
use InPost\International\Shipment\Message\BulkUpdateShipmentStatusesCommand;
use InPost\International\Shipment\Message\GetTrackingDetailsCommand;
use InPost\International\Shipment\Message\PrintLabelCommand;
use InPost\International\Shipment\Message\UpdateShipmentStatusCommand;
use InPost\International\Shipment\View\TrackingDetailsViewFactory;
use PrestaShop\PrestaShop\Core\Grid\Definition\Factory\FilterableGridDefinitionFactoryInterface;
use PrestaShop\PrestaShop\Core\Grid\GridFactoryInterface;
use PrestaShop\PrestaShop\Core\Grid\Presenter\GridPresenterInterface;
use PrestaShopBundle\Security\Annotation\AdminSecurity;
use PrestaShopBundle\Service\Grid\ResponseBuilder;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
use Symfony\Component\HttpFoundation\StreamedResponse;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\Routing\Annotation\Route;

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

/**
 * @Route(path="shipments", name="admin_inpost_intl_shipments_", defaults={"_legacy_controller"=ShipmentController::TAB_NAME})
 */
final class ShipmentController extends AbstractController
{
    public const TAB_NAME = 'AdminInPostInternationalShipments';

    /**
     * @Route(name="index", methods={"GET"})
     *
     * @AdminSecurity("is_granted('read', request.get('_legacy_controller'))")
     */
    public function index(ShipmentFilters $filters, ApiConfigurationRepositoryInterface $configuration, GridFactoryInterface $gridFactory, GridPresenterInterface $gridPresenter): Response
    {
        $filters->addFilter([
            'is_sandbox' => $configuration->getConfiguration()->getEnvironment()->isTestEnvironment(),
        ]);

        $grid = $gridFactory->getGrid($filters);
        $pickupTimeForm = $this->createForm(CutoffTimeRequestType::class, null, [
            'action' => $url = $this->generateUrl('admin_inpost_intl_pickup_orders_check_cutoff_time'),
            'method' => 'GET',
        ]);

        parse_str(parse_url($url, PHP_URL_QUERY), $query);
        $token = $query['_token'] ?? null;

        return $this->render('@Modules/inpostinternational/views/templates/admin/shipment/index.html.twig', [
            'grid' => $gridPresenter->present($grid),
            'pickup_time_form' => $pickupTimeForm->createView(),
            'user_token' => $token,
            'layoutTitle' => $this->trans('Shipments', [], 'Modules.Inpostinternational.Shipment'),
        ]);
    }

    /**
     * @Route(name="filter", methods={"POST"})
     *
     * @AdminSecurity("is_granted('read', request.get('_legacy_controller'))", redirectRoute="admin_module_manage")
     */
    public function filter(Request $request, FilterableGridDefinitionFactoryInterface $gridDefinitionFactory, ResponseBuilder $responseBuilder): Response
    {
        return $responseBuilder
            ->buildSearchResponse(
                $gridDefinitionFactory,
                $request,
                $gridDefinitionFactory->getFilterId(),
                'admin_inpost_intl_shipments_index'
            )
            ->setStatusCode(Response::HTTP_SEE_OTHER);
    }

    /**
     * @Route(path="/{id}/tracking", name="tracking", methods={"GET"}, requirements={"id"="\d+"})
     *
     * @AdminSecurity("is_granted('read', request.get('_legacy_controller'))")
     */
    public function tracking(int $id, TrackingDetailsViewFactory $viewFactory): Response
    {
        try {
            /** @var Parcel|null $parcel */
            $parcel = $this->handle(new GetTrackingDetailsCommand($id));

            if (null === $parcel) {
                return new JsonResponse([
                    'message' => $this->trans('Tracking details were not found in the API.', [], 'Modules.Inpostinternational.Shipment'),
                ], Response::HTTP_NOT_FOUND);
            }

            return $this->render('@Modules/inpostinternational/views/templates/admin/shipment/_tracking.html.twig', [
                'parcel' => $viewFactory->create($parcel),
            ]);
        } catch (\Throwable $e) {
            $message = $e instanceof ApiClientExceptionInterface
                ? $this->trans('Could not fetch tracking details from the API.', [], 'Modules.Inpostinternational.Shipment')
                : $this->getErrorMessageForException($e);

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

    /**
     * @Route(path="/{id}/print-label", name="print_label", methods={"GET"}, requirements={"id"="\d+"})
     *
     * @AdminSecurity("is_granted('read', request.get('_legacy_controller'))")
     */
    public function printLabel(int $id): StreamedResponse
    {
        try {
            /** @var StreamableInterface $label */
            $label = $this->handle(new PrintLabelCommand($id));

            return $this->streamFile($label);
        } catch (ShipmentNotFoundException $e) {
            throw $this->createNotFoundException('Shipment does not exist.', $e);
        } catch (ApiClientExceptionInterface $e) {
            throw new HttpException(Response::HTTP_BAD_GATEWAY, 'Could not download the label from the API.', $e);
        } catch (\Throwable $e) {
            $this->getLogger()->critical('An error occurred while printing shipment label: {exception}', [
                'exception' => $e,
            ]);

            throw $e;
        }
    }

    /**
     * @Route(path="/{id}/update-status", name="update_status", methods={"POST"}, requirements={"id"="\d+"})
     *
     * @AdminSecurity("is_granted('read', request.get('_legacy_controller'))")
     */
    public function updateStatus(int $id, Request $request): Response
    {
        try {
            $status = $this->handle(new UpdateShipmentStatusCommand($id));
            $message = $this->trans('Successful update.', [], 'Admin.Notifications.Success');

            if ($request->isXmlHttpRequest()) {
                $status = 'CREATED' === $status ? TrackingEvent::CREATED : $status;

                return new JsonResponse([
                    'status' => (new TrackingEvent($status))->trans($this->get('translator')),
                    'message' => $message,
                ]);
            }

            $this->addFlash('success', $message);
        } catch (\Throwable $e) {
            $message = $e instanceof ApiClientExceptionInterface
                ? $this->trans('Could not retrieve shipment status from the API.', [], 'Modules.Inpostinternational.Shipment')
                : $this->getErrorMessageForException($e);

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

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

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

    /**
     * @Route(path="/print-labels", name="bulk_print_labels", methods={"POST"})
     *
     * @AdminSecurity("is_granted('read', request.get('_legacy_controller'))")
     */
    public function bulkPrintLabels(Request $request): StreamedResponse
    {
        try {
            /** @var StreamableInterface $labels */
            $labels = $this->handle(new BulkPrintLabelsCommand(...$this->getShipmentIds($request)));

            return $this->streamFile($labels);
        } catch (ApiClientExceptionInterface $e) {
            throw new HttpException(Response::HTTP_BAD_GATEWAY, 'Could not download labels from the API.', $e);
        } catch (\Throwable $e) {
            $this->getLogger()->critical('An error occurred while printing shipment labels: {exception}', [
                'exception' => $e,
            ]);

            throw $e;
        }
    }

    /**
     * @Route(path="/update-statuses", name="bulk_update_status", methods={"POST"})
     *
     * @AdminSecurity("is_granted('read', request.get('_legacy_controller'))")
     */
    public function bulkUpdateStatus(Request $request): Response
    {
        try {
            $this->handle(new BulkUpdateShipmentStatusesCommand(...$this->getShipmentIds($request)));
            $this->addFlash('success', $this->trans('Successful update.', [], 'Admin.Notifications.Success'));
        } catch (NoShipmentsFoundException $e) {
            throw $this->createNotFoundException('No shipments found.', $e);
        } catch (ApiClientExceptionInterface $e) {
            $this->addFlash('error', $this->trans('Could not retrieve shipment status from the API.', [], 'Modules.Inpostinternational.Shipment'));
        } catch (\Throwable $e) {
            $this->addFlash('error', $this->getErrorMessageForException($e));
        }

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

    private function streamFile(StreamableInterface $file): StreamedResponse
    {
        $response = new StreamedResponse([$file, 'stream']);

        $disposition = $response->headers->makeDisposition(ResponseHeaderBag::DISPOSITION_ATTACHMENT, $file->getFilename());
        $response->headers->add([
            'Content-Type' => $file->getContentType(),
            'Content-Disposition' => $disposition,
        ]);

        return $response;
    }

    private function getShipmentIds(Request $request): array
    {
        $shipmentIds = RequestHelper::getAll($request->request, ShipmentGridDefinitionFactory::BULK_IDS_INPUT_NAME);

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

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

    private function getErrorMessageForException(\Throwable $e): string
    {
        if ($e instanceof ShipmentNotFoundException) {
            return $this->trans('Shipment was not found.', [], 'Modules.Inpostinternational.Shipment');
        }

        $this->getLogger()->critical('An error occurred while updating the shipment status: {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 ShipmentNotFoundException:
                return Response::HTTP_NOT_FOUND;
            case $e instanceof ApiClientExceptionInterface:
                return Response::HTTP_BAD_GATEWAY;
            default:
                return Response::HTTP_INTERNAL_SERVER_ERROR;
        }
    }
}
