<?php

declare(strict_types=1);

namespace Gls\GlsPoland\Controller\Admin;

use Gls\GlsPoland\Configuration\Api;
use Gls\GlsPoland\Consignment\Exception\ConsignmentAlreadyPickedUpException;
use Gls\GlsPoland\Download\SpreadsheetFile;
use Gls\GlsPoland\Download\StreamableFileInterface;
use Gls\GlsPoland\Form\Type\BulkDeleteConsignmentsType;
use Gls\GlsPoland\Form\Type\CreatePickupType;
use Gls\GlsPoland\Message\CreatePickupCommand;
use Gls\GlsPoland\Message\DeleteConsignmentCommand;
use Gls\GlsPoland\Message\PrintLabelsCommand;
use Gls\GlsPoland\Message\PrintPickupReceiptCommand;
use Gls\GlsPoland\PrestaShop\Grid\Definition\Factory\FilterableGridDefinitionFactoryInterface;
use Gls\GlsPoland\PrestaShop\Grid\Filters\ConsignmentFilters;
use Gls\GlsPoland\Translation\TranslatorAwareTrait;
use PrestaShop\PrestaShop\Core\CommandBus\CommandBusInterface;
use PrestaShop\PrestaShop\Core\Grid\Column\ColumnInterface;
use PrestaShop\PrestaShop\Core\Grid\Filter\GridFilterFormFactoryInterface;
use PrestaShop\PrestaShop\Core\Grid\GridFactoryInterface;
use PrestaShop\PrestaShop\Core\Grid\GridInterface;
use PrestaShop\PrestaShop\Core\Grid\Presenter\GridPresenterInterface;
use PrestaShopBundle\Security\Annotation\AdminSecurity;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
use Symfony\Component\HttpFoundation\StreamedResponse;
use Symfony\Component\OptionsResolver\Exception\NoSuchOptionException;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\Routing\Annotation\Route;

/**
 * @Route(path="consignments", name="admin_gls_poland_consignments_", defaults={"_legacy_controller"=ConsignmentController::TAB_NAME, "_legacy_link"=ConsignmentController::TAB_NAME})
 */
final class ConsignmentController extends AbstractController
{
    use TranslatorAwareTrait;

    public const TAB_NAME = 'AdminGlsPolandConsignments';
    public const PARENT_TAB_NAME = 'AdminParentShipping';

    /**
     * @Route(path="/", name="index", methods={"GET"})
     * @AdminSecurity("is_granted('read', request.get('_legacy_controller'))")
     */
    public function index(ConsignmentFilters $filters, Api\ConfigurationInterface $apiConfiguration, GridFactoryInterface $gridFactory, GridPresenterInterface $gridPresenter): Response
    {
        $filters->addFilter([
            'sandbox' => $apiConfiguration->isSandboxEnabled(),
        ]);

        $grid = $gridFactory->getGrid($filters);
        $createPickupForm = $this->createForm(CreatePickupType::class, new CreatePickupCommand(date('Y-m-d h:i:s')), [
            'action' => $this->generateUrl('admin_gls_poland_consignments_create_pickup'),
        ]);
        $bulkDeleteForm = $this->createForm(BulkDeleteConsignmentsType::class, ['consignmentIds' => []], [
            'action' => $this->generateUrl('admin_gls_poland_consignments_bulk_delete'),
        ]);

        return $this->render('@Modules/glspoland/views/templates/admin/consignment/index.html.twig', [
            'grid' => $gridPresenter->present($grid),
            'create_pickup_form' => $createPickupForm->createView(),
            'bulk_delete_form' => $bulkDeleteForm->createView(),
        ]);
    }

    /**
     * @Route("/", name="filter", methods={"POST"})
     * @AdminSecurity("is_granted('read', request.get('_legacy_controller'))")
     */
    public function filter(Request $request, FilterableGridDefinitionFactoryInterface $gridDefinitionFactory, GridFilterFormFactoryInterface $filterFormFactory): Response
    {
        $gridDefinition = $gridDefinitionFactory->getDefinition();
        $filtersForm = $filterFormFactory
            ->create($gridDefinition)
            ->handleRequest($request);

        $redirectParams = [];
        if ($filtersForm->isSubmitted()) {
            $redirectParams[$gridDefinitionFactory->getFilterId()] = [
                'filters' => $filtersForm->getData(),
            ];
        }

        return $this->redirectToRoute('admin_gls_poland_consignments_index', $redirectParams, Response::HTTP_SEE_OTHER);
    }

    /**
     * @Route(path="/export.{_format}", name="export", methods={"GET"}, defaults={"_format": "csv"}, requirements={"_format": "csv|xls|xlsx"})
     * @AdminSecurity("is_granted('read', request.get('_legacy_controller'))")
     */
    public function export(ConsignmentFilters $filters, Api\ConfigurationInterface $apiConfiguration, GridFactoryInterface $gridFactory, string $_format = 'csv'): StreamedResponse
    {
        $filters = new ConsignmentFilters(['limit' => null] + $filters->all());
        $filters->addFilter([
            'sandbox' => $apiConfiguration->isSandboxEnabled(),
        ]);

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

        $file = new SpreadsheetFile($_format, function () use ($grid) {
            yield $namesByField = $this->getGridColumnNamesByField($grid);

            foreach ($grid->getData()->getRecords() as $record) {
                yield array_merge($namesByField, array_intersect_key($record, $namesByField));
            }
        });

        return $this->streamFile('consignments', $file);
    }

    /**
     * @Route("/{id}/delete", name="delete", methods={"POST"}, requirements={"id"="\d+"})
     * @AdminSecurity("is_granted('delete', request.get('_legacy_controller'))", redirectRoute="admin_gls_poland_consignments_index")
     */
    public function delete(int $id, CommandBusInterface $bus): Response
    {
        try {
            $bus->handle(new DeleteConsignmentCommand($id));
            $this->addFlash('success', $this->getTranslator()->trans('Successful deletion', [], 'Admin.Notifications.Success'));
        } catch (ConsignmentAlreadyPickedUpException $e) {
            $this->addFlash('error', $this->getTranslator()->trans(
                'Consignment #{id} cannot be deleted because it already belongs to pickup #{pickup}.',
                [
                    '#{id}' => $e->getConsignmentId(),
                    '#{pickup}' => $e->getPickupId(),
                ],
                'Modules.Glspoland.Errors'
            ));
        }

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

    /**
     * @Route("/{id}/print-labels", name="print_labels", methods={"GET"}, requirements={"id"="\d+"})
     * @AdminSecurity("is_granted('read', request.get('_legacy_controller'))")
     */
    public function printLabel(int $id, CommandBusInterface $bus): StreamedResponse
    {
        $labels = $bus->handle(new PrintLabelsCommand($id));

        return $this->streamFile('labels', $labels);
    }

    /**
     * @Route("/create-pickup", name="create_pickup", methods={"POST"})
     * @AdminSecurity("is_granted('update', request.get('_legacy_controller'))", redirectRoute="admin_gls_poland_consignments_index")
     */
    public function createPickup(Request $request, CommandBusInterface $bus): Response
    {
        $form = $this
            ->createForm(CreatePickupType::class)
            ->handleRequest($request);

        if ($form->isSubmitted() && $form->isValid()) {
            $bus->handle($form->getData());
            $this->addFlash('success', $this->getTranslator()->trans('Successful creation', [], 'Admin.Notifications.Success'));
        } else {
            foreach ($form->getErrors(true) as $error) {
                $this->addFlash('error', sprintf('%s: %s', $error->getOrigin()->getName(), $error->getMessage()));
            }
        }

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

    /**
     * @Route("/{id}/print-pickup-receipt", name="print_pickup_receipt", methods={"GET"}, requirements={"id"="\d+"})
     * @AdminSecurity("is_granted('read', request.get('_legacy_controller'))")
     */
    public function printPickupReceipt(int $id, CommandBusInterface $bus): Response
    {
        $receipt = $bus->handle(new PrintPickupReceiptCommand($id));

        return $this->streamFile('pickup_receipt', $receipt);
    }
    /**
     * @Route("/bulk-delete", name="bulk_delete", methods={"POST"})
     * @AdminSecurity("is_granted('delete', request.get('_legacy_controller'))", redirectRoute="admin_gls_poland_consignments_index")
     */
    public function bulkDelete(Request $request, CommandBusInterface $bus): Response
    {
        $form = $this->createForm(BulkDeleteConsignmentsType::class)->handleRequest($request);
        if ($form->isSubmitted() && $form->isValid()) {
            $consignmentIds = $form->get('consignmentIds')->getData();
        } else {
            $consignmentIds = $this->getConsignmentIdsFromRequest($request);
        }

        $deletedCount = 0;

        foreach ($consignmentIds as $consignmentId) {
            try {
                $bus->handle(new DeleteConsignmentCommand($consignmentId));
                ++$deletedCount;
            } catch (ConsignmentAlreadyPickedUpException $e) {
                $this->addFlash('error', $this->getTranslator()->trans(
                    'Consignment #{id} cannot be deleted because it already belongs to pickup #{pickup}.',
                    [
                        '#{id}' => $e->getConsignmentId(),
                        '#{pickup}' => $e->getPickupId(),
                    ],
                    'Modules.Glspoland.Errors'
                ));
            } catch (\Throwable $e) {
                $this->addFlash('error', $this->getTranslator()->trans(
                    'Consignment #{id}: {error}',
                    [
                        '#{id}' => $consignmentId,
                        '{error}' => $e->getMessage(),
                    ],
                    'Modules.Glspoland.Errors'
                ));
            }
        }

        if ($deletedCount > 0) {
            $this->addFlash('success', $this->getTranslator()->trans('Successful deletion', [], 'Admin.Notifications.Success'));
        }

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

    private function getConsignmentIdsFromRequest(Request $request, string $key = 'gls_poland_consignment_ids'): array
    {
        $consignmentIds = [];

        if (method_exists($request->request, 'all')) {
            $all = $request->request->all();
            if (isset($all[$key]) && is_array($all[$key])) {
                $consignmentIds = $all[$key];
            }
        }

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

        $normalizedConsignmentIds = [];
        foreach ($consignmentIds as $consignmentId) {
            if (!is_scalar($consignmentId) || '' === (string) $consignmentId) {
                continue;
            }

            $consignmentId = (int) $consignmentId;
            if ($consignmentId > 0) {
                $normalizedConsignmentIds[] = $consignmentId;
            }
        }

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

        return $normalizedConsignmentIds;
    }
    private function streamFile(string $fileName, StreamableFileInterface $file): StreamedResponse
    {
        $response = new StreamedResponse([$file, 'stream']);

        $disposition = $response->headers->makeDisposition(
            ResponseHeaderBag::DISPOSITION_ATTACHMENT,
            sprintf('%s.%s', $fileName, $file->getFileExtension())
        );

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

        return $response;
    }

    private function getGridColumnNamesByField(GridInterface $grid): array
    {
        $names = [];

        /** @var ColumnInterface $column */
        foreach ($grid->getDefinition()->getColumns() as $column) {
            try {
                $field = $column->getOption('field');
            } catch (NoSuchOptionException $e) {
                continue;
            }

            $names[$field] = $column->getName();
        }

        return $names;
    }
}
