<?php

declare(strict_types=1);

/*
 * This file is part of the Ferienpass package.
 *
 * (c) Richard Henkenjohann <richard@ferienpass.online>
 *
 * For more information visit the project website <https://ferienpass.online>
 * or the documentation under <https://docs.ferienpass.online>.
 */

namespace Ferienpass\AdminBundle\Controller\Page;

use Ferienpass\AdminBundle\Breadcrumb\Breadcrumb;
use Ferienpass\AdminBundle\Export\ExportQueryBuilderInterface;
use Ferienpass\AdminBundle\Form\Filter\DebtorFilter;
use Ferienpass\AdminBundle\Form\Filter\OverpaidPaymentsFilter;
use Ferienpass\AdminBundle\Form\Filter\PaymentsFilter;
use Ferienpass\AdminBundle\Form\Filter\UnpaidPaymentsFilter;
use Ferienpass\AdminBundle\List\ListCriteriaApplier;
use Ferienpass\AdminBundle\LiveComponent\MultiSelect;
use Ferienpass\AdminBundle\LiveComponent\MultiSelectAction;
use Ferienpass\AdminBundle\LiveComponent\MultiSelectHandlerInterface;
use Ferienpass\AdminBundle\Routing\Base58;
use Ferienpass\CoreBundle\Entity\Edition;
use Ferienpass\CoreBundle\Entity\User;
use Ferienpass\CoreBundle\Enum\PaymentStatus;
use Ferienpass\CoreBundle\Export\Payments\ReceiptPdfExport;
use Ferienpass\CoreBundle\Facade\PaymentsFacade;
use Ferienpass\CoreBundle\Repository\AttendanceRepository;
use Ferienpass\CoreBundle\Repository\BookEntryRepository;
use Ferienpass\CoreBundle\Repository\DebtorRepository;
use Ferienpass\CoreBundle\Repository\EditionRepository;
use Ferienpass\CoreBundle\Repository\PaymentRepository;
use FOS\HttpCacheBundle\Configuration\InvalidateRoute;
use Psr\EventDispatcher\EventDispatcherInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\DependencyInjection\Attribute\TaggedLocator;
use Symfony\Component\DependencyInjection\ServiceLocator;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Routing\Requirement\Requirement;
use Symfony\Component\Security\Http\Attribute\IsGranted;
use Symfony\Component\Uid\Uuid;

#[IsGranted('ROLE_ADMIN')]
#[Route('/zahlungen', defaults: ['_requires_payments' => true])]
final class PaymentsController extends AbstractController implements MultiSelectHandlerInterface
{
    public function __construct(private readonly ReceiptPdfExport $receiptExport, #[TaggedLocator(ExportQueryBuilderInterface::class, defaultIndexMethod: 'getFormat')] private readonly ServiceLocator $exporters)
    {
    }

    #[Route('/belege/{edition?}', name: 'admin_payments_receipts', priority: -2, requirements: ['edition' => '[a-zA-Z0-9_\-+]+'])]
    public function index(?string $edition, PaymentRepository $repository, Breadcrumb $breadcrumb, EditionRepository $editions): Response
    {
        [$selectedEditions, $allEditions] = $this->resolveEditions($edition, $editions);

        $qb = $repository->createQueryBuilder('i')
            ->where('i.status <> :status_abandoned')
            ->setParameter('status_abandoned', PaymentStatus::ABANDONED)
            ->orderBy('i.createdAt', 'DESC')
        ;

        if ($selectedEditions) {
            $qb
                ->innerJoin('i.items', 'pi_edition')
                ->innerJoin('pi_edition.attendance', 'a_edition')
                ->innerJoin('a_edition.offer', 'o_edition')
                ->andWhere('o_edition.edition IN (:selectedEditions)')
                ->setParameter('selectedEditions', $selectedEditions)
            ;
        }

        return $this->render('@FerienpassAdmin/page/payments/index.html.twig', [
            'qb' => $qb,
            'filterType' => PaymentsFilter::class,
            'exports' => array_keys($this->exporters->getProvidedServices()),
            'searchable' => ['billingAddress', 'billingEmail', 'receiptNumber'],
            'editions' => $allEditions,
            'selectedEditions' => $selectedEditions,
            'config' => $this->receiptsListConfig(),
            'breadcrumb' => $breadcrumb->generate(['payments.title', ['route' => 'admin_payments_receipts']]),
        ]);
    }

    #[Route('/debitoren/{edition?}', name: 'admin_payments_debtors', priority: -2, requirements: ['edition' => '[a-zA-Z0-9_\-+]+'])]
    public function debtors(?string $edition, BookEntryRepository $bookEntryRepository, DebtorRepository $debtorsRepository, Breadcrumb $breadcrumb, EventDispatcherInterface $dispatcher, EditionRepository $editions): Response
    {
        [$selectedEditions, $allEditions] = $this->resolveEditions($edition, $editions);

        $sumByDay = $bookEntryRepository->createQueryBuilder('entry')
            ->select('SUM(entry.value) as sum')
            ->addSelect("DATE_FORMAT(entry.bookingDate, '%Y-%m-%d') AS date")
            ->where('entry.type = :type')
            ->setParameter('type', 'payment')
            ->groupBy('date')
            ->orderBy('date', 'ASC')
            ->getQuery()
            ->getResult()
        ;

        $chart = [];
        foreach (new \DatePeriod(new \DateTime('-1 year'), new \DateInterval('P1D'), new \DateTime('tomorrow')) as $date) {
            $chart['labels'][] = $date->format('d.m.Y');
            $entry = array_find($sumByDay, static fn (array $v) => $v['date'] === $date->format('Y-m-d'));
            $chart['values'][] = $entry ? (int) ($entry['sum']) / 100 : 0;
        }

        $qb = $debtorsRepository->createQueryBuilder('i')
            ->leftJoin('i.user', 'user')
        ;

        // Solve N+1
        $qb
            ->addSelect('user')
            ->leftJoin('user.participants', 'participant')
            ->addSelect('participant')
            ->leftJoin('participant.attendances', 'attendance')
            ->addSelect('attendance')
            ->leftJoin('i.bookEntries', 'entry')
            ->addSelect('entry')
        ;

        if ($selectedEditions) {
            $qb
                ->innerJoin('attendance.offer', 'o_edition')
                ->andWhere('o_edition.edition IN (:selectedEditions)')
                ->setParameter('selectedEditions', $selectedEditions)
            ;
        }

        return $this->render('@FerienpassAdmin/page/payments/debtors.html.twig', [
            'qb' => $qb,
            'filterType' => DebtorFilter::class,
            'searchable' => ['user.firstname', 'user.lastname'],
            'chart' => $chart,
            'dispatcher' => $dispatcher,
            'editions' => $allEditions,
            'selectedEditions' => $selectedEditions,
            'config' => $this->debtorsListConfig(),
            'breadcrumb' => $breadcrumb->generate(['payments.title', ['route' => 'admin_payments_receipts']], 'payments.debtors'),
        ]);
    }

    #[Route('/ausstehend/{edition?}', name: 'admin_payments_unpaid', priority: -2, requirements: ['edition' => '[a-zA-Z0-9_\-+]+'])]
    public function unpaid(?string $edition, AttendanceRepository $attendances, Breadcrumb $breadcrumb, EventDispatcherInterface $dispatcher, EditionRepository $editions): Response
    {
        [$selectedEditions, $allEditions] = $this->resolveEditions($edition, $editions);

        $qb = $attendances->createQueryBuilder('attendance')
            ->innerJoin('attendance.offer', 'o')
            ->innerJoin('attendance.participant', 'participant')
            ->leftJoin('attendance.activity', 'activity')
            ->leftJoin('activity.createdBy', 'activityUser')
            ->addSelect('participant')
            ->addSelect('o')
            ->where('o.fee > 0')
        ;

        PaymentsFacade::addWhereLogic($qb);

        if ($selectedEditions) {
            $qb->andWhere('o.edition IN (:selectedEditions)')->setParameter('selectedEditions', $selectedEditions);
        }

        return $this->render('@FerienpassAdmin/page/payments/unpaid.html.twig', [
            'qb' => $qb,
            'filterType' => UnpaidPaymentsFilter::class,
            'searchable' => ['o.name', 'participant.firstname', 'participant.lastname'],
            'dispatcher' => $dispatcher,
            'editions' => $allEditions,
            'selectedEditions' => $selectedEditions,
            'config' => $this->unpaidListConfig(),
            'breadcrumb' => $breadcrumb->generate(['payments.title', ['route' => 'admin_payments_receipts']], 'payments.unpaid'),
        ]);
    }

    #[Route('/überzahlt/{edition?}', name: 'admin_payments_overpaid', priority: -2, requirements: ['edition' => '[a-zA-Z0-9_\-+]+'])]
    public function overpaid(?string $edition, AttendanceRepository $attendances, Breadcrumb $breadcrumb, EventDispatcherInterface $dispatcher, EditionRepository $editions): Response
    {
        [$selectedEditions, $allEditions] = $this->resolveEditions($edition, $editions);

        $qb = $attendances->createQueryBuilder('attendance')
            ->innerJoin('attendance.paymentItems', 'pi')
            ->innerJoin('pi.payment', 'p')
            ->leftJoin('attendance.offer', 'o')
            ->leftJoin('attendance.participant', 'participant')
            ->groupBy('attendance.id')
            ->where('attendance.payable = 1')
            ->andWhere('p.status = :paid')
            ->setParameter('paid', PaymentStatus::PAID)
            ->having('SUM(CASE WHEN pi.amount > 0 THEN 1 ELSE 0 END) - SUM(CASE WHEN pi.amount < 0 THEN 1 ELSE 0 END) > 2')
        ;

        if ($selectedEditions) {
            $qb->andWhere('o.edition IN (:selectedEditions)')->setParameter('selectedEditions', $selectedEditions);
        }

        return $this->render('@FerienpassAdmin/page/payments/overpaid.html.twig', [
            'qb' => $qb,
            'filterType' => OverpaidPaymentsFilter::class,
            'searchable' => ['o.name', 'participant.firstname', 'participant.lastname'],
            'dispatcher' => $dispatcher,
            'editions' => $allEditions,
            'selectedEditions' => $selectedEditions,
            'config' => $this->overpaidListConfig(),
            'breadcrumb' => $breadcrumb->generate(['payments.title', ['route' => 'admin_payments_receipts']], 'payments.overpaid'),
        ]);
    }

    #[Route('/debitoren/{uuid}', name: 'admin_payments_debtor', requirements: ['uuid' => Base58::UUID])]
    #[Route('/debitoren/{uuid}', name: 'admin_payments_debtor_legacy', requirements: ['uuid' => Requirement::UUID])]
    public function debtor(Uuid $uuid, DebtorRepository $debtorRepository, Breadcrumb $breadcrumb, EventDispatcherInterface $dispatcher, AttendanceRepository $attendanceRepository): Response
    {
        $debtor = $debtorRepository->findOneBy(['uuid' => $uuid]);
        if (null === $debtor) {
            throw $this->createNotFoundException();
        }

        $qb = $attendanceRepository->createQueryBuilder('attendance');
        $qb
            ->innerJoin('attendance.participant', 'participant')
            ->innerJoin('attendance.offer', 'offer')
            ->where('participant.user = :user')
            ->setParameter('user', $debtor->getUser())
        ;
        PaymentsFacade::addWhereLogic($qb);

        $ms = new MultiSelect([
            new MultiSelectAction('settle', 'admin_attendances_settle', method: 'GET', turboFrame: 'slideover'),
        ], self::class);

        return $this->render('@FerienpassAdmin/page/payments/debtor.html.twig', [
            'debtor' => $debtor,
            'qb' => $qb,
            'ms' => $ms,
            'dispatcher' => $dispatcher,
            'breadcrumb' => $breadcrumb->generate(['payments.title', ['route' => 'admin_payments_receipts']], ['payments.debtors', ['route' => 'admin_payments_debtors']], $debtor->getUser()?->getName() ?: 'payments.debtor_unknown'),
        ]);
    }

    public function handleMultiSelect(string $action, array $selected, Request $request): Response
    {
        throw new \RuntimeException('Code should not be reached');
    }

    #[Route('/debitoren/{uuid}/freie-buchung', name: 'admin_payments_create_booking', requirements: ['uuid' => Base58::UUID])]
    #[Route('/debitoren/{uuid}/freie-buchung', name: 'admin_payments_create_booking_legacy', requirements: ['uuid' => Requirement::UUID])]
    #[InvalidateRoute('admin_payments_debtor', params: ['uuid' => ['expression' => 'uuid']])]
    public function createBooking(Uuid $uuid, DebtorRepository $debtorRepository, Breadcrumb $breadcrumb): Response
    {
        $debtor = $debtorRepository->findOneBy(['uuid' => $uuid]);
        if (null === $debtor) {
            throw $this->createNotFoundException();
        }

        return $this->render('@FerienpassAdmin/page/payments/createBooking.html.twig', [
            'debtor' => $debtor,
            'breadcrumb' => $breadcrumb->generate(
                ['payments.title', ['route' => 'admin_payments_receipts']],
                ['payments.debtors', ['route' => 'admin_payments_debtors']],
                $debtor->getUser()?->getName() ?: 'payments.debtor_unknown',
                //    [$debtor->getUser()?->getName() ?: 'payments.debtor_unknown', ['route' => 'admin_payments_debtor', 'id' => $debtor->getId()]],
                'Freie Buchung'
            ),
        ]);
    }

    #[Route('/export.{format}', name: 'admin_payments_export', requirements: ['format' => '\w+'])]
    public function export(PaymentRepository $repository, string $format, Request $request, ListCriteriaApplier $filterer)
    {
        $qb = $repository->createQueryBuilder('i')
            ->where('i.status <> :status_abandoned')
            ->setParameter('status_abandoned', PaymentStatus::ABANDONED)
        ;

        $filterer->apply(
            $qb,
            PaymentsFilter::class,
            ['billingAddress', 'billingEmail', 'receiptNumber'],
            $request->query->get('q'),
            $request->query->get('sort'),
            (array) $request->query->all('filter')
        );

        if (!$this->exporters->has($format)) {
            throw $this->createNotFoundException();
        }

        $exporter = $this->exporters->get($format);

        return $this->file($exporter->generate($qb), "zahlungen.$format");
    }

    #[Route('/belege/{uuid}', name: 'admin_payments_receipt', requirements: ['uuid' => Base58::UUID])]
    #[Route('/belege/{uuid}', name: 'admin_payments_receipt_legacy', requirements: ['uuid' => Requirement::UUID])]
    public function show(Uuid $uuid, PaymentRepository $paymentRepository, Breadcrumb $breadcrumb): Response
    {
        $payment = $paymentRepository->findOneBy(['uuid' => $uuid]);
        if (null === $payment) {
            throw $this->createNotFoundException();
        }

        return $this->render('@FerienpassAdmin/page/payments/receipt.html.twig', [
            'payment' => $payment,
            'breadcrumb' => $breadcrumb->generate(['payments.title', ['route' => 'admin_payments_receipts']], 'Beleg #'.$payment->getReceiptNumber()),
        ]);
    }

    #[Route('/{uuid}/storno', name: 'admin_payments_reverse', requirements: ['uuid' => Base58::UUID])]
    #[Route('/{uuid}/storno', name: 'admin_payments_reverse_legacy', requirements: ['uuid' => Requirement::UUID])]
    #[InvalidateRoute('admin_payments_receipt', params: ['uuid' => ['expression' => 'uuid']])]
    public function reverse(Uuid $uuid, PaymentRepository $paymentRepository, Request $request, Breadcrumb $breadcrumb): Response
    {
        $payment = $paymentRepository->findOneBy(['uuid' => $uuid]);
        if (null === $payment) {
            throw $this->createNotFoundException();
        }

        if ($payment->isReversal()) {
            throw new NotFoundHttpException('Payment exists, but is a reversal payment that cannot be reversed.');
        }

        return $this->render('@FerienpassAdmin/page/payments/reverse.html.twig', [
            'payment' => $payment,
            'items' => (array) $request->query->all('items'),
            'breadcrumb' => $breadcrumb->generate(['payments.title', ['route' => 'admin_payments_receipts']], 'Beleg #'.$payment->getReceiptNumber(), 'Storno'),
        ]);
    }

    #[Route('/belege/{uuid}.pdf', name: 'admin_payments_receipt_pdf', requirements: ['uuid' => Base58::UUID])]
    #[Route('/belege/{uuid}.pdf', name: 'admin_payments_receipt_pdf_legacy', requirements: ['uuid' => Requirement::UUID])]
    public function pdf(Uuid $uuid, PaymentRepository $paymentRepository): Response
    {
        $payment = $paymentRepository->findOneBy(['uuid' => $uuid]);
        if (null === $payment) {
            throw $this->createNotFoundException();
        }

        $path = $this->receiptExport->generate($payment);

        return $this->file($path, \sprintf('beleg-%s.pdf', $payment->getId()));
    }

    private function receiptsListConfig(): array
    {
        return [
            'squeezedColumns' => ['user'],
            'title' => <<<'TWIG'
                <span class="text-gray-700 dark:text-gray-400 font-normal mr-1">{{ item.createdAt|default ? item.createdAt|date("d.m.Y") : "" }}</span>
                {{ item.billingAddress|split("\n")|first }}
                {{ component("Admin:Badge", { payment: item.status, class: "ml-2" }) }}
                TWIG,
            'info' => [
                '{{ item.receiptNumber|default("") }}',
            ],
            'aside' => '<div class="text-teal-700 dark:text-teal-400 font-semibold leading-6 shrink-0">{{ ((item.totalAmount|default(0))/100)|format_currency("EUR", locale="de") }}</div>',
            'columns' => [
                'receiptNumber' => '<a href="{{ path("admin_payments_receipt", { uuid: item.uuid.toBase58 }) }}" data-turbo-frame="slideover" class="hover:bg-gray-100 dark:hover:bg-gray-800 rounded-md px-1 py-0.5 -mx-1 -my-0.5 transition-colors">{{ item.receiptNumber|default("") }}</a>',
                'billingName' => '{{ item.billingAddress|default("")|split("\n")|first }}',
                'date' => '{{ item.createdAt|default ? item.createdAt|date("d.m.Y H:i") : "" }}',
                'amount' => '{{ ((item.totalAmount|default(0))/100)|format_currency("EUR", locale="de") }}',
                'user' => <<<'TWIG'
                    {% if item.user|default %}
                    <span class="inline-flex items-center justify-center h-6 w-6 rounded-full bg-gray-400">
                        <span class="text-xs font-medium leading-none text-white">{{ item.user.firstname|first|upper ~ item.user.lastname|first|upper }}</span>
                    </span>
                    <span>{{ item.user.firstname ~ " " ~ item.user.lastname }}</span>
                    {% else %}-{% endif %}
                    TWIG,
                'status' => '{{ component("Admin:Badge", { payment: item.status, size: "sm", class: "-ml-2" }) }}',
            ],
        ];
    }

    private function debtorsListConfig(): array
    {
        return [
            'rightAlignedColumns' => ['balance', 'due', 'total'],
            'title' => <<<'TWIG'
                <div class="flex items-center space-x-2">
                {% if item.user|default %}
                <span class="inline-flex items-center justify-center h-6 w-6 rounded-full bg-gray-400 shrink-0">
                    <span class="text-xs font-medium leading-none text-white">{{ item.user.firstname|first|upper ~ item.user.lastname|first|upper }}</span>
                </span>
                {% endif %}
                <span>{{ item.user.name|default("") }}</span>
                </div>
                TWIG,
            'info' => [
                '{% set debit = (item.getDueSum(dispatcher))/100*(-1) %}{% set balance = item.balance/100 %}{% set debit_balance = debit+balance %}Gesamt: {{ debit_balance|format_currency("EUR", locale="de") }}',
                'Guthaben: {{ (item.balance/100)|format_currency("EUR", locale="de") }}',
            ],
            'aside' => '<div class="text-teal-700 dark:text-teal-400 font-semibold leading-6 shrink-0">{% set debit = (item.getDueSum(dispatcher))/100*(-1) %}{% set balance = item.balance/100 %}{% set debit_balance = debit+balance %}{{ debit_balance|format_currency("EUR", locale="de") }}</div>',
            'columns' => [
                'debtor' => <<<'TWIG'
                    <a href="{{ path("admin_payments_debtor", { uuid: item.uuid.toBase58 }) }}" class="hover:bg-gray-200/75 dark:hover:bg-gray-800 hover:text-gray-900 dark:hover:text-gray-100 rounded-md px-1.5 py-1 -mx-1.5 -my-1 transition-colors font-medium text-gray-900 dark:text-gray-100" data-turbo-frame="slideover">{{ item.user.name|default("") ?: item.id }}</a>
                    TWIG,
                'balance' => <<<'TWIG'
                    {% set balance = item.balance/100 %}
                    <p class="font-medium {{ balance > 0 ? 'text-green-600' : (balance < 0 ? 'text-red-600' : 'text-gray-600') }}">
                        {{ balance|format_currency("EUR", locale="de") }}
                    </p>
                    TWIG,
                'due' => <<<'TWIG'
                    {% set debit = (item.getDueSum(dispatcher))/100*(-1) %}
                    <p class="font-medium {{ debit > 0 ? 'text-green-600' : (debit < 0 ? 'text-red-600' : 'text-gray-600') }}">
                        {{ debit|format_currency("EUR", locale="de") }}
                    </p>
                    TWIG,
                'total' => <<<'TWIG'
                    {% set debit = (item.getDueSum(dispatcher))/100*(-1) %}
                    {% set balance = item.balance/100 %}
                    {% set debit_balance = debit+balance %}
                    <p class="font-medium {{ debit_balance > 0 ? 'text-green-600' : (debit_balance < 0 ? 'text-red-600' : 'text-gray-600') }}">
                        {{ debit_balance|format_currency("EUR", locale="de") }}
                    </p>
                    TWIG,
            ],
        ];
    }

    private function unpaidListConfig(): array
    {
        return [
            'title' => <<<'TWIG'
                <div class="flex items-center space-x-2">
                {% if item.participant|default %}
                <span class="inline-flex items-center justify-center h-6 w-6 rounded-full bg-gray-400 shrink-0">
                    <span class="text-xs font-medium leading-none text-white">{{ item.participant.firstname|first|upper ~ item.participant.lastname|first|upper }}</span>
                </span>
                {% endif %}
                <span>{{ item.participant.name|default("") }}</span>
                </div>
                TWIG,
            'info' => [
                '{{ item.offer.name }}',
                '{{ (item.offer.getFeePayable(item.participant, dispatcher)/100)|format_currency("EUR", locale="de") }}',
            ],
            'columns' => [
                'participant' => <<<'TWIG'
                    <div class="flex items-center space-x-2">
                    {% if item.participant|default %}
                    <span class="inline-flex items-center justify-center h-6 w-6 rounded-full bg-gray-400 shrink-0">
                        <span class="text-xs font-medium leading-none text-white">{{ item.participant.firstname|first|upper ~ item.participant.lastname|first|upper }}</span>
                    </span>
                    {% endif %}
                    <span>{{ item.participant.name|default("") }}</span>
                    </div>
                    TWIG,
                'offer' => <<<'TWIG'
                    <p class="font-medium text-gray-900">
                        {{ item.offer.name|default("") }}
                        {{ component("Admin:Badge", { attendance: item.status, size: "xs", class: "ml-1" }) }}
                    </p>
                    <p>
                        {% if item.offer.dates|first.begin %}
                            {{ item.offer.dates|first.begin|format_datetime(pattern="E, dd.MM.yyyy HH:mm") }}
                        {% else %}
                            <span class="inline-flex items-center text-orange-600">
                                {{ ux_icon('ph:warning', {class: 'mr-1 size-4'}) }}
                                <span>Termin fehlt</span>
                            </span>
                        {% endif %}
                    </p>
                    {% if item.confirmed %}
                        {% set log = item.activity|filter(a => a.transitionName == "create")|last %}
                        {% if log is not null %}
                            <p class="text-xs text-gray-500">angemeldet am {{ log.createdAt|default ? log.createdAt|format_datetime(pattern="dd.MM HH:mm") : "" }}{% if log.createdBy.name|default %} von {{ log.createdBy.name }}{% endif %}</p>
                        {% endif %}
                        {% set log = item.activity|filter(a => a.transitionName == "confirm")|last %}
                        {% if log is not null %}
                            <p class="text-xs text-gray-500">bestätigt am {{ log.createdAt|default ? log.createdAt|format_datetime(pattern="dd.MM HH:mm") : "" }}{% if log.createdBy.name|default %} von {{ log.createdBy.name }}{% endif %}</p>
                        {% endif %}
                    {% endif %}
                    {% if item.withdrawn %}
                        {% set log = item.activity|filter(a => a.transitionName == "withdraw")|last %}
                        {% if log is not null %}
                            <p class="text-xs text-gray-500">abgemeldet am {{ log.createdAt|default ? log.createdAt|format_datetime(pattern="dd.MM HH:mm") : "" }}{% if log.createdBy.name|default %} von {{ log.createdBy.name }}{% endif %}</p>
                        {% endif %}
                    {% endif %}
                    TWIG,
                'amount' => '<span class="font-medium block text-red-600">{{ (item.offer.getFeePayable(item.participant, dispatcher)/100)|format_currency("EUR", locale="de") }}</span>',
            ],
        ];
    }

    private function overpaidListConfig(): array
    {
        return [
            'title' => <<<'TWIG'
                <div class="flex items-center space-x-2">
                {% if item.participant|default %}
                <span class="inline-flex items-center justify-center h-6 w-6 rounded-full bg-gray-400 shrink-0">
                    <span class="text-xs font-medium leading-none text-white">{{ item.participant.firstname|first|upper ~ item.participant.lastname|first|upper }}</span>
                </span>
                {% endif %}
                <span>{{ item.participant.name|default("") }}</span>
                </div>
                TWIG,
            'info' => [
                '{{ item.offer.name }}',
                '{{ item.offer.dates|first.begin ? (item.offer.dates|first.begin)|format_datetime(pattern="E, dd.MM.yyyy HH:mm") : ("<span class=\"inline-flex items-center text-orange-600\">" ~ ux_icon("ph:warning", {class: "mr-1 size-4"}) ~ "<span>Termin fehlt</span></span>")|raw }}',
            ],
            'columns' => [
                'participant' => <<<'TWIG'
                    <div class="flex items-center space-x-2">
                    {% if item.participant|default %}
                    <span class="inline-flex items-center justify-center h-6 w-6 rounded-full bg-gray-400 shrink-0">
                        <span class="text-xs font-medium leading-none text-white">{{ item.participant.firstname|first|upper ~ item.participant.lastname|first|upper }}</span>
                    </span>
                    {% endif %}
                    <span>{{ item.participant.name|default("") }}</span>
                    </div>
                    TWIG,
                'offer' => <<<'TWIG'
                    <p class="font-medium text-gray-900">
                        {{ item.offer.name|default("") }}
                        {{ component("Admin:Badge", { attendance: item.status, size: "xs", class: "ml-1" }) }}
                    </p>
                    <p>
                        {% if item.offer.dates|first.begin %}
                            {{ item.offer.dates|first.begin|format_datetime(pattern="E, dd.MM.yyyy HH:mm") }}
                        {% else %}
                            <span class="inline-flex items-center text-orange-600">
                                {{ ux_icon('ph:warning', {class: 'mr-1 size-4'}) }}
                                <span>Termin fehlt</span>
                            </span>
                        {% endif %}
                    </p>
                    TWIG,
                'payments' => <<<'TWIG'
                    {% for pi in item.paymentItems|filter(i => i.payment.paid) %}
                        <p class="whitespace-nowrap">
                            <a target="_blank" href="{{ path('admin_payments_receipt', { uuid: pi.payment.uuid.toBase58 }) }}" class="text-gray-600 hover:text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-800 hover:text-gray-900 dark:hover:text-gray-100 rounded-md px-1 py-0.5 -mx-1 -my-0.5 transition-colors">#{{ pi.payment.receiptNumber }}</a>
                            <span class="{{ pi.amount >= 0 ? 'text-green-600' : 'text-red-600' }} font-medium">{{ (pi.amount/100)|format_currency("EUR", locale="de") }}</span>
                        </p>
                    {% endfor %}
                    TWIG,
            ],
        ];
    }

    /**
     * Parse the optional `{edition?}` route segment into a list of selected
     * Edition entities, and return the full list of editions visible to the
     * current user. 404 if any of the requested aliases doesn't exist.
     *
     * @return array{0: Edition[], 1: Edition[]}
     */
    private function resolveEditions(?string $edition, EditionRepository $editions): array
    {
        $aliases = $edition ? array_values(array_filter(explode('+', $edition))) : [];
        $selectedEditions = $aliases ? $editions->findBy(['alias' => $aliases]) : [];
        if ($aliases && \count($selectedEditions) !== \count($aliases)) {
            throw $this->createNotFoundException();
        }

        /** @var User $user */
        $user = $this->getUser();
        $allEditions = $editions->findBy([], ['createdAt' => 'DESC']);
        if (!$this->isGranted('ROLE_ADMIN')) {
            $userHost = $user->getHosts()->first() ?: null;
            $allEditions = array_values(array_filter($allEditions, static fn (Edition $e) => $e->getHosts()->isEmpty() || ($userHost && $e->getHosts()->contains($userHost))));
        }

        return [$selectedEditions, $allEditions];
    }
}
