<?php

namespace App\Controller;

use App\Entity\Attestation;
use App\Entity\Billet;
use App\Entity\BilletBuyer;
use App\Entity\BilletBuyerItem;
use App\Entity\BilletDesign;
use App\Entity\BilletOrder;
use App\Entity\Category;
use App\Entity\Event;
use App\Entity\Payout;
use App\Entity\User;
use App\Service\AuditService;
use App\Service\BilletOrderService;
use App\Service\EventIndexService;
use App\Service\ExportService;
use App\Service\MailerService;
use App\Service\OrderIndexService;
use App\Service\PayoutPdfService;
use App\Service\StripeService;
use Doctrine\ORM\EntityManagerInterface;
use Knp\Component\Pager\PaginatorInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Http\Attribute\IsGranted;

#[IsGranted('ROLE_USER')]
class AccountController extends AbstractController
{
    private const BREADCRUMB_HOME = ['name' => 'Accueil', 'url' => '/'];
    private const BREADCRUMB_ACCOUNT = ['name' => 'Mon compte', 'url' => '/mon-compte'];
    private const EVENT_BASE_URL = '/mon-compte/evenement/';
    private const EVENT_CATEGORIES_SUFFIX = '/modifier?tab=categories';
    private const DQL_EXCLUDE_INVITATIONS = 'o.isInvitation = false OR o.isInvitation IS NULL';
    private const DQL_BB_EXCLUDE_INVITATIONS = 'bb.isInvitation = false OR bb.isInvitation IS NULL';
    private const CONTENT_TYPE_PDF = 'application/pdf';
    private const PDF_SUFFIX = '.pdf"';

    #[Route('/mon-compte', name: 'app_account')]
    public function index(Request $request, StripeService $stripeService, EntityManagerInterface $em, PaginatorInterface $paginator, EventIndexService $eventIndex): Response
    {
        /** @var User $user */
        $user = $this->getUser();
        $isOrganizer = $this->isGranted('ROLE_ORGANIZER');
        if ($isOrganizer && $user->getStripeAccountId() && (!$user->isStripeChargesEnabled() || !$user->isStripePayoutsEnabled())) {
            try { // @codeCoverageIgnoreStart
                $account = $stripeService->getClient()->accounts->retrieve($user->getStripeAccountId());
                $user->setStripeChargesEnabled((bool) $account->charges_enabled);
                $user->setStripePayoutsEnabled((bool) $account->payouts_enabled);
                $em->flush();
            } catch (\Throwable) {
                // Stripe API unavailable, keep current status
            } // @codeCoverageIgnoreEnd
        }

        $stripeReady = $user->isStripeChargesEnabled() && $user->isStripePayoutsEnabled();
        $organizerTabs = ['events', 'subaccounts', 'payouts'];
        $defaultTab = ($isOrganizer && $stripeReady) ? 'events' : 'tickets';
        $tab = $request->query->getString('tab', $defaultTab);

        if (\in_array($tab, $organizerTabs, true) && !$stripeReady) {
            $tab = $defaultTab;
        }

        $payouts = [];
        $subAccounts = [];
        $events = [];
        if ($isOrganizer) {
            $payouts = $em->getRepository(Payout::class)->findBy(
                ['organizer' => $user],
                ['createdAt' => 'DESC'],
            );
            $subAccounts = $em->getRepository(User::class)->findBy(
                ['parentOrganizer' => $user],
                ['createdAt' => 'DESC'],
            );
            $searchQuery = $request->query->getString('q', '');
            $eventsQuery = $eventIndex->searchEvents('event_'.$user->getId(), $searchQuery, ['account' => $user]);
            $events = $paginator->paginate($eventsQuery, $request->query->getInt('page', 1), 10);
        }

        $financeStats = $isOrganizer
            ? $this->computeFinanceStats($user, $em)
            : ['paid' => 0, 'pending' => 0, 'refunded' => 0, 'cancelled' => 0, 'commissionEticket' => 0, 'commissionStripe' => 0, 'net' => 0];

        $orders = $em->getRepository(BilletBuyer::class)->findBy(
            ['user' => $user],
            ['createdAt' => 'DESC'],
        );

        $userTickets = [];
        foreach ($orders as $order) {
            $tickets = $em->getRepository(BilletOrder::class)->findBy(['billetBuyer' => $order]);
            foreach ($tickets as $ticket) {
                $userTickets[] = $ticket;
            }
        }

        return $this->render('account/index.html.twig', [
            'tab' => $tab,
            'isOrganizer' => $isOrganizer,
            'payouts' => $payouts,
            'subAccounts' => $subAccounts,
            'events' => $events,
            'orders' => $orders,
            'userTickets' => $userTickets,
            'financeStats' => $financeStats,
            'breadcrumbs' => [
                self::BREADCRUMB_HOME,
                self::BREADCRUMB_ACCOUNT,
            ],
        ]);
    }

    #[Route('/mon-compte/parametres', name: 'app_account_settings', methods: ['POST'])]
    public function settings(Request $request, EntityManagerInterface $em): Response
    {
        /** @var User $user */
        $user = $this->getUser();
        $isOrganizer = $this->isGranted('ROLE_ORGANIZER');

        if (!$isOrganizer) {
            $user->setFirstName(trim($request->request->getString('first_name')));
            $user->setLastName(trim($request->request->getString('last_name')));
        }

        $user->setEmail(trim($request->request->getString('email')));
        $user->setPhone(trim($request->request->getString('phone')));

        if (!$isOrganizer) {
            $user->setAddress(trim($request->request->getString('address')));
            $user->setPostalCode(trim($request->request->getString('postal_code')));
            $user->setCity(trim($request->request->getString('city')));
        }

        if ($isOrganizer) {
            $user->setWebsite(trim($request->request->getString('website')));
            $user->setFacebook(trim($request->request->getString('facebook')));
            $user->setInstagram(trim($request->request->getString('instagram')));
            $user->setTwitter(trim($request->request->getString('twitter')));
            $user->setTiktok(trim($request->request->getString('tiktok')));

            $logoFile = $request->files->get('logo');
            if ($logoFile) {
                $user->setLogoFile($logoFile);
            }
        }

        $em->flush();

        $this->addFlash('success', 'Parametres mis a jour.');

        return $this->redirectToRoute('app_account', ['tab' => 'settings']);
    }

    /** @codeCoverageIgnore Requires live Stripe API */
    #[Route('/mon-compte/abonnement', name: 'app_account_billing_subscribe')]
    public function billingSubscribe(StripeService $stripeService, EntityManagerInterface $em): Response
    {
        /** @var User $user */
        $user = $this->getUser();

        if (!$this->isGranted('ROLE_ORGANIZER') || !$user->isBilling() || 'poor' !== $user->getBillingState()) {
            return $this->redirectToRoute('app_account');
        }

        try {
            $url = $stripeService->createBillingCheckoutSession($user);

            return $this->redirect($url);
        } catch (\Throwable $e) {
            $this->addFlash('error', 'Erreur lors de la creation de l\'abonnement : '.$e->getMessage());

            return $this->redirectToRoute('app_account');
        }
    }

    /** @codeCoverageIgnore Stripe checkout callback */
    #[Route('/mon-compte/abonnement/succes', name: 'app_account_billing_success')]
    public function billingSuccess(): Response
    {
        $this->addFlash('success', 'Votre abonnement a ete active avec succes.');

        return $this->redirectToRoute('app_account');
    }

    /** @codeCoverageIgnore Requires live Stripe API */
    #[Route('/mon-compte/stripe-connect', name: 'app_account_stripe_connect')]
    public function stripeConnect(StripeService $stripeService, EntityManagerInterface $em): Response
    {
        /** @var User $user */
        $user = $this->getUser();

        if (!$this->isGranted('ROLE_ORGANIZER') || ($user->isBilling() && 'good' !== $user->getBillingState())) {
            return $this->redirectToRoute('app_account');
        }

        try {
            if (!$user->getStripeAccountId()) {
                $accountId = $stripeService->createAccountConnect($user);
                $user->setStripeAccountId($accountId);
                $user->setStripeStatus('started');
                $em->flush();
            }

            $link = $stripeService->createAccountLink($user->getStripeAccountId());

            return $this->redirect($link);
        } catch (\Throwable $e) {
            $this->addFlash('error', 'Erreur lors de la connexion a Stripe : '.$e->getMessage());

            return $this->redirectToRoute('app_account');
        }
    }

    /** @codeCoverageIgnore Requires live Stripe API */
    #[Route('/mon-compte/stripe-cancel', name: 'app_account_stripe_cancel', methods: ['POST'])]
    public function stripeCancel(StripeService $stripeService, EntityManagerInterface $em): Response
    {
        /** @var User $user */
        $user = $this->getUser();

        if ($this->isGranted('ROLE_ORGANIZER') && $user->getStripeAccountId()) {
            try {
                $stripeService->deleteAccount($user->getStripeAccountId());
            } catch (\Throwable) {
                // Account may already be deleted on Stripe side
            }

            $user->setStripeAccountId(null);
            $user->setStripeChargesEnabled(false);
            $user->setStripePayoutsEnabled(false);
            $em->flush();

            $this->addFlash('success', 'Compte Stripe cloture.');
        }

        return $this->redirectToRoute('app_account');
    }

    /** @codeCoverageIgnore Stripe redirect callback */
    #[Route('/stripe/connect/return', name: 'app_stripe_connect_return')]
    public function stripeConnectReturn(): Response
    {
        $this->addFlash('success', 'Configuration Stripe terminee.');

        return $this->redirectToRoute('app_account');
    }

    /** @codeCoverageIgnore Stripe redirect callback */
    #[Route('/stripe/connect/refresh', name: 'app_stripe_connect_refresh')]
    public function stripeConnectRefresh(): Response
    {
        return $this->redirectToRoute('app_account_stripe_connect');
    }

    #[Route('/mon-compte/sous-compte/creer', name: 'app_account_create_subaccount', methods: ['POST'])]
    public function createSubAccount(Request $request, EntityManagerInterface $em, UserPasswordHasherInterface $passwordHasher, MailerService $mailerService): Response
    {
        /** @var User $user */
        $user = $this->getUser();

        if (!$this->isGranted('ROLE_ORGANIZER')) {
            return $this->redirectToRoute('app_account');
        }

        $plainPassword = bin2hex(random_bytes(8));

        $subAccount = new User();
        $subAccount->setFirstName(trim($request->request->getString('first_name')));
        $subAccount->setLastName(trim($request->request->getString('last_name')));
        $subAccount->setEmail(trim($request->request->getString('email')));
        $subAccount->setPassword($passwordHasher->hashPassword($subAccount, $plainPassword));
        $subAccount->setIsVerified(true);
        $subAccount->setEmailVerifiedAt(new \DateTimeImmutable());
        $subAccount->setParentOrganizer($user);

        $permissions = $request->request->all('permissions');
        $subAccount->setSubAccountPermissions($permissions);

        $em->persist($subAccount);
        $em->flush();

        $mailerService->sendEmail(
            to: $subAccount->getEmail(),
            subject: 'Votre sous-compte E-Ticket a ete cree',
            content: $this->renderView('email/subaccount_created.html.twig', [
                'firstName' => $subAccount->getFirstName(),
                'organizerName' => $user->getCompanyName() ?? $user->getFirstName().' '.$user->getLastName(),
                'email' => $subAccount->getEmail(),
                'password' => $plainPassword,
                'permissions' => $permissions,
            ]),
            withUnsubscribe: false,
        );

        $this->addFlash('success', sprintf('Sous-compte %s %s cree.', $subAccount->getFirstName(), $subAccount->getLastName()));

        return $this->redirectToRoute('app_account', ['tab' => 'subaccounts']);
    }

    #[Route('/mon-compte/sous-compte/{id}', name: 'app_account_edit_subaccount_page', methods: ['GET'])]
    public function editSubAccountPage(User $subAccount): Response
    {
        /** @var User $user */
        $user = $this->getUser();

        if ($subAccount->getParentOrganizer()?->getId() !== $user->getId()) {
            throw $this->createAccessDeniedException();
        }

        return $this->render('account/edit_subaccount.html.twig', [
            'subAccount' => $subAccount,
            'breadcrumbs' => [
                self::BREADCRUMB_HOME,
                self::BREADCRUMB_ACCOUNT,
                ['name' => 'Sous-compte', 'url' => self::BREADCRUMB_ACCOUNT['url'].'/sous-compte/'.$subAccount->getId()],
            ],
        ]);
    }

    #[Route('/mon-compte/sous-compte/{id}/modifier', name: 'app_account_edit_subaccount', methods: ['POST'])]
    public function editSubAccount(User $subAccount, Request $request, EntityManagerInterface $em): Response
    {
        /** @var User $user */
        $user = $this->getUser();

        if ($subAccount->getParentOrganizer()?->getId() !== $user->getId()) {
            throw $this->createAccessDeniedException();
        }

        $permissions = $request->request->all('permissions');
        $subAccount->setSubAccountPermissions($permissions);
        $subAccount->setFirstName(trim($request->request->getString('first_name')));
        $subAccount->setLastName(trim($request->request->getString('last_name')));
        $subAccount->setEmail(trim($request->request->getString('email')));
        $em->flush();

        $this->addFlash('success', sprintf('Sous-compte %s %s mis a jour.', $subAccount->getFirstName(), $subAccount->getLastName()));

        return $this->redirectToRoute('app_account', ['tab' => 'subaccounts']);
    }

    #[Route('/mon-compte/sous-compte/{id}/supprimer', name: 'app_account_delete_subaccount', methods: ['POST'])]
    public function deleteSubAccount(User $subAccount, EntityManagerInterface $em): Response
    {
        /** @var User $user */
        $user = $this->getUser();

        if ($subAccount->getParentOrganizer()?->getId() !== $user->getId()) {
            throw $this->createAccessDeniedException();
        }

        $name = sprintf('%s %s', $subAccount->getFirstName(), $subAccount->getLastName());
        $em->remove($subAccount);
        $em->flush();

        $this->addFlash('success', sprintf('Sous-compte %s supprime.', $name));

        return $this->redirectToRoute('app_account', ['tab' => 'subaccounts']);
    }

    #[Route('/mon-compte/evenement/creer', name: 'app_account_create_event', methods: ['GET', 'POST'])]
    public function createEvent(Request $request, EntityManagerInterface $em, EventIndexService $eventIndex, AuditService $audit): Response
    {
        $this->denyAccessUnlessGranted('ROLE_ORGANIZER');
        if ($redirect = $this->requireStripeReady()) { // @codeCoverageIgnoreStart
            return $redirect;
        } // @codeCoverageIgnoreEnd

        /** @var User $user */
        $user = $this->getUser();

        if ($request->isMethod('POST')) {
            $event = new Event();
            $event->setAccount($user);
            $this->hydrateEventFromRequest($event, $request);

            $em->persist($event);
            $em->flush();

            $eventIndex->indexEvent($event);

            $audit->log('event_created', 'Event', $event->getId(), ['title' => $event->getTitle()]);

            $this->addFlash('success', 'Evenement cree avec succes.');

            return $this->redirectToRoute('app_account', ['tab' => 'events']);
        }

        return $this->render('account/create_event.html.twig', [
            'breadcrumbs' => [
                self::BREADCRUMB_HOME,
                self::BREADCRUMB_ACCOUNT,
                ['name' => 'Creer un evenement', 'url' => '/mon-compte/evenement/creer'],
            ],
        ]);
    }

    #[Route('/mon-compte/evenement/{id}/modifier', name: 'app_account_edit_event', methods: ['GET', 'POST'])]
    public function editEvent(Event $event, Request $request, EntityManagerInterface $em, EventIndexService $eventIndex, PaginatorInterface $paginator, OrderIndexService $orderIndex, AuditService $audit): Response
    {
        $this->denyAccessUnlessGranted('ROLE_ORGANIZER');
        if ($redirect = $this->requireStripeReady()) { // @codeCoverageIgnoreStart
            return $redirect;
        } // @codeCoverageIgnoreEnd

        $user = $this->requireEventOwnership($event);

        if ($request->isMethod('POST')) {
            $this->hydrateEventFromRequest($event, $request);

            $em->flush();

            $eventIndex->indexEvent($event);

            $audit->log('event_updated', 'Event', $event->getId(), ['title' => $event->getTitle()]);

            $this->addFlash('success', 'Evenement modifie avec succes.');

            return $this->redirectToRoute('app_account_edit_event', ['id' => $event->getId()]);
        }

        $categories = $em->getRepository(Category::class)->findBy(
            ['event' => $event],
            ['position' => 'ASC'],
        );

        $allBillets = $em->getRepository(Billet::class)->findBy(['category' => $categories], ['position' => 'ASC']);

        $billets = [];
        $billetIds = [];
        foreach ($allBillets as $billet) {
            $catId = $billet->getCategory()->getId();
            $billets[$catId][] = $billet;
            $billetIds[] = $billet->getId();
        }

        $soldCounts = [];
        if ($billetIds) {
            $rows = $em->createQueryBuilder()
                ->select('IDENTITY(bo.billet) AS billetId, COUNT(bo.id) AS cnt')
                ->from(BilletOrder::class, 'bo')
                ->join('bo.billetBuyer', 'bb')
                ->where('bo.billet IN (:ids)')
                ->andWhere(self::DQL_BB_EXCLUDE_INVITATIONS)
                ->setParameter('ids', $billetIds)
                ->groupBy('bo.billet')
                ->getQuery()
                ->getArrayResult();

            foreach ($rows as $row) {
                $soldCounts[$row['billetId']] = (int) $row['cnt'];
            }
        }

        $searchQuery = $request->query->getString('q', '');
        $ordersQuery = '' !== $searchQuery
            ? $orderIndex->searchOrders($event->getId(), $searchQuery)
            : $em->getRepository(BilletBuyer::class)->findBy(['event' => $event], ['createdAt' => 'DESC']);
        $eventOrders = $paginator->paginate($ordersQuery, $request->query->getInt('page', 1), 20);

        $ticketsSearchQuery = $request->query->getString('tq', '');
        $ticketsQb = $em->createQueryBuilder()
            ->select('t', 'bb')
            ->from(BilletOrder::class, 't')
            ->join('t.billetBuyer', 'bb')
            ->where('bb.event = :ticketEvent')
            ->setParameter('ticketEvent', $event)
            ->orderBy('t.createdAt', 'DESC');

        if ('' !== $ticketsSearchQuery) {
            $ticketsQb->andWhere('t.reference LIKE :tq OR t.securityKey LIKE :tq OR t.billetName LIKE :tq OR bb.firstName LIKE :tq OR bb.lastName LIKE :tq OR bb.email LIKE :tq')
                ->setParameter('tq', '%'.$ticketsSearchQuery.'%');
        }

        $eventTickets = $paginator->paginate($ticketsQb->getQuery(), $request->query->getInt('tp', 1), 20);

        $paidEventOrders = $em->createQueryBuilder()
            ->select('o', 'i')
            ->from(BilletBuyer::class, 'o')
            ->leftJoin('o.items', 'i')
            ->where('o.event = :event')
            ->andWhere('o.status = :status')
            ->andWhere(self::DQL_EXCLUDE_INVITATIONS)
            ->setParameter('event', $event)
            ->setParameter('status', BilletBuyer::STATUS_PAID)
            ->getQuery()
            ->getResult();
        $eventStats = $this->computeEventStats($paidEventOrders);

        return $this->render('account/edit_event.html.twig', [
            'event' => $event,
            'owner' => $user,
            'categories' => $categories,
            'billets' => $billets,
            'sold_counts' => $soldCounts,
            'commission_rate' => $user->getCommissionRate() ?? 0,
            'stripe_fee_rate' => (float) $this->getParameter('stripe_fee_rate'),
            'stripe_fee_fixed' => (int) $this->getParameter('stripe_fee_fixed'),
            'billet_design' => $em->getRepository(BilletDesign::class)->findOneBy(['event' => $event]),
            'event_orders' => $eventOrders,
            'event_tickets' => $eventTickets,
            'tickets_search_query' => $ticketsSearchQuery,
            'invitations' => $em->getRepository(BilletBuyer::class)->findBy(['event' => $event, 'isInvitation' => true], ['createdAt' => 'DESC']),
            'event_total_ht' => $eventStats['totalHT'] / 100,
            'event_total_sold' => $eventStats['totalSold'],
            'event_total_orders' => \count($paidEventOrders),
            'billet_stats' => $eventStats['billetStats'],
            'search_query' => $searchQuery,
            'breadcrumbs' => [
                self::BREADCRUMB_HOME,
                self::BREADCRUMB_ACCOUNT,
                ['name' => $event->getTitle(), 'url' => self::EVENT_BASE_URL.$event->getId().'/modifier'],
            ],
        ]);
    }

    #[Route('/mon-compte/evenement/{id}/categorie/ajouter', name: 'app_account_event_add_category', methods: ['POST'])]
    public function addCategory(Event $event, Request $request, EntityManagerInterface $em, AuditService $audit): Response
    {
        $this->denyAccessUnlessGranted('ROLE_ORGANIZER');
        if ($redirect = $this->requireStripeReady()) { // @codeCoverageIgnoreStart
            return $redirect;
        } // @codeCoverageIgnoreEnd

        $this->requireEventOwnership($event);

        $name = trim($request->request->getString('name'));
        if ('' === $name) {
            $this->addFlash('error', 'Le nom de la categorie est requis.');

            return $this->redirectToRoute('app_account_edit_event', ['id' => $event->getId(), 'tab' => 'categories']);
        }

        $maxPosition = $em->getRepository(Category::class)->count(['event' => $event]);

        $category = new Category();
        $category->setName($name);
        $category->setEvent($event);
        $category->setPosition($maxPosition);

        $startAt = $request->request->getString('start_at');
        if ('' !== $startAt) {
            $category->setStartAt(new \DateTimeImmutable($startAt));
        }

        $endAt = $request->request->getString('end_at');
        if ('' !== $endAt) {
            $category->setEndAt(new \DateTimeImmutable($endAt));
        }

        if ($category->getEndAt() < $category->getStartAt()) {
            $category->setEndAt($category->getStartAt()->modify('+30 days'));
        }

        $em->persist($category);
        $em->flush();

        $audit->log('category_created', 'Category', $category->getId(), ['name' => $name, 'event' => $event->getTitle()]);

        $this->addFlash('success', sprintf('Categorie "%s" ajoutee.', $name));

        return $this->redirectToRoute('app_account_edit_event', ['id' => $event->getId(), 'tab' => 'categories']);
    }

    #[Route('/mon-compte/evenement/{id}/categorie/{categoryId}/modifier', name: 'app_account_event_edit_category', methods: ['GET', 'POST'])]
    public function editCategory(Event $event, int $categoryId, Request $request, EntityManagerInterface $em, AuditService $audit): Response
    {
        $this->denyAccessUnlessGranted('ROLE_ORGANIZER');
        if ($redirect = $this->requireStripeReady()) { // @codeCoverageIgnoreStart
            return $redirect;
        } // @codeCoverageIgnoreEnd

        $this->requireEventOwnership($event);
        $category = $this->requireCategory($categoryId, $event, $em);

        if ($request->isMethod('POST')) {
            $category->setName(trim($request->request->getString('name')));

            $startAt = $request->request->getString('start_at');
            if ('' !== $startAt) {
                $category->setStartAt(new \DateTimeImmutable($startAt));
            }

            $endAt = $request->request->getString('end_at');
            if ('' !== $endAt) {
                $category->setEndAt(new \DateTimeImmutable($endAt));
            }

            if ($category->getEndAt() < $category->getStartAt()) {
                $category->setEndAt($category->getStartAt()->modify('+30 days'));
            }

            $category->setIsHidden($request->request->getBoolean('is_hidden'));

            $em->flush();

            $audit->log('category_updated', 'Category', $category->getId(), ['name' => $category->getName(), 'event' => $event->getTitle()]);

            $this->addFlash('success', 'Categorie modifiee.');

            return $this->redirectToRoute('app_account_edit_event', ['id' => $event->getId(), 'tab' => 'categories']);
        }

        return $this->render('account/edit_category.html.twig', [
            'event' => $event,
            'category' => $category,
            'breadcrumbs' => [
                self::BREADCRUMB_HOME,
                self::BREADCRUMB_ACCOUNT,
                ['name' => $event->getTitle(), 'url' => self::EVENT_BASE_URL.$event->getId().self::EVENT_CATEGORIES_SUFFIX],
                ['name' => $category->getName(), 'url' => ''],
            ],
        ]);
    }

    #[Route('/mon-compte/evenement/{id}/categorie/{categoryId}/supprimer', name: 'app_account_event_delete_category', methods: ['POST'])]
    public function deleteCategory(Event $event, int $categoryId, EntityManagerInterface $em, AuditService $audit): Response
    {
        $this->denyAccessUnlessGranted('ROLE_ORGANIZER');
        if ($redirect = $this->requireStripeReady()) { // @codeCoverageIgnoreStart
            return $redirect;
        } // @codeCoverageIgnoreEnd

        $this->requireEventOwnership($event);

        $category = $em->getRepository(Category::class)->find($categoryId);
        if ($category && $category->getEvent()->getId() === $event->getId()) {
            $catName = $category->getName();
            $catId = $category->getId();
            $em->remove($category);
            $em->flush();
            $audit->log('category_deleted', 'Category', $catId, ['name' => $catName, 'event' => $event->getTitle()]);
            $this->addFlash('success', 'Categorie supprimee.');
        }

        return $this->redirectToRoute('app_account_edit_event', ['id' => $event->getId(), 'tab' => 'categories']);
    }

    #[Route('/mon-compte/evenement/{id}/categorie/reorder', name: 'app_account_event_reorder_categories', methods: ['POST'])]
    public function reorderCategories(Event $event, Request $request, EntityManagerInterface $em): Response
    {
        $this->denyAccessUnlessGranted('ROLE_ORGANIZER');
        if ($redirect = $this->requireStripeReady()) { // @codeCoverageIgnoreStart
            return $redirect;
        } // @codeCoverageIgnoreEnd

        $this->requireEventOwnership($event);

        $order = json_decode($request->getContent(), true);
        if (\is_array($order)) {
            foreach ($order as $position => $categoryId) {
                $category = $em->getRepository(Category::class)->find($categoryId);
                if ($category && $category->getEvent()->getId() === $event->getId()) {
                    $category->setPosition($position);
                }
            }
            $em->flush();
        }

        return $this->json(['success' => true]);
    }

    #[Route('/mon-compte/evenement/{id}/categorie/{categoryId}/billet/ajouter', name: 'app_account_event_add_billet', methods: ['GET', 'POST'])]
    public function addBillet(Event $event, int $categoryId, Request $request, EntityManagerInterface $em, StripeService $stripeService, AuditService $audit): Response
    {
        $this->denyAccessUnlessGranted('ROLE_ORGANIZER');
        if ($redirect = $this->requireStripeReady()) { // @codeCoverageIgnoreStart
            return $redirect;
        } // @codeCoverageIgnoreEnd

        $user = $this->requireEventOwnership($event);
        $category = $this->requireCategory($categoryId, $event, $em);

        if ($request->isMethod('POST')) {
            $billet = new Billet();
            $billet->setCategory($category);
            $billet->setPosition($em->getRepository(Billet::class)->count(['category' => $category]));
            $this->hydrateBilletFromRequest($billet, $request);

            $em->persist($billet);
            $this->syncBilletToStripe($billet, $user, $stripeService);
            $em->flush();

            $audit->log('billet_created', 'Billet', $billet->getId(), ['name' => $billet->getName(), 'event' => $event->getTitle()]);

            $this->addFlash('success', 'Billet ajoute avec succes.');

            return $this->redirectToRoute('app_account_edit_event', ['id' => $event->getId(), 'tab' => 'categories']);
        }

        return $this->render('account/add_billet.html.twig', [
            'event' => $event,
            'category' => $category,
            'allowedTypes' => self::getAllowedBilletTypes($user->getOffer()),
            'breadcrumbs' => [
                self::BREADCRUMB_HOME,
                self::BREADCRUMB_ACCOUNT,
                ['name' => $event->getTitle(), 'url' => self::EVENT_BASE_URL.$event->getId().self::EVENT_CATEGORIES_SUFFIX],
                ['name' => 'Ajouter un billet'],
            ],
        ]);
    }

    #[Route('/mon-compte/evenement/{id}/billet/{billetId}/modifier', name: 'app_account_event_edit_billet', methods: ['GET', 'POST'])]
    public function editBillet(Event $event, int $billetId, Request $request, EntityManagerInterface $em, StripeService $stripeService, AuditService $audit): Response
    {
        $this->denyAccessUnlessGranted('ROLE_ORGANIZER');
        if ($redirect = $this->requireStripeReady()) { // @codeCoverageIgnoreStart
            return $redirect;
        } // @codeCoverageIgnoreEnd

        $user = $this->requireEventOwnership($event);

        $billet = $em->getRepository(Billet::class)->find($billetId);
        if (!$billet || $billet->getCategory()->getEvent()->getId() !== $event->getId()) {
            throw $this->createNotFoundException();
        }

        if ($request->isMethod('POST')) {
            $this->hydrateBilletFromRequest($billet, $request);
            $this->syncBilletToStripe($billet, $user, $stripeService);
            $em->flush();

            $audit->log('billet_updated', 'Billet', $billet->getId(), ['name' => $billet->getName(), 'event' => $event->getTitle()]);

            $this->addFlash('success', 'Billet modifie avec succes.');

            return $this->redirectToRoute('app_account_edit_event', ['id' => $event->getId(), 'tab' => 'categories']);
        }

        return $this->render('account/edit_billet.html.twig', [
            'event' => $event,
            'billet' => $billet,
            'allowedTypes' => self::getAllowedBilletTypes($user->getOffer()),
            'breadcrumbs' => [
                self::BREADCRUMB_HOME,
                self::BREADCRUMB_ACCOUNT,
                ['name' => $event->getTitle(), 'url' => self::EVENT_BASE_URL.$event->getId().self::EVENT_CATEGORIES_SUFFIX],
                ['name' => 'Modifier un billet'],
            ],
        ]);
    }

    #[Route('/mon-compte/evenement/{id}/billet/{billetId}/supprimer', name: 'app_account_event_delete_billet', methods: ['POST'])]
    public function deleteBillet(Event $event, int $billetId, EntityManagerInterface $em, StripeService $stripeService, AuditService $audit): Response
    {
        $this->denyAccessUnlessGranted('ROLE_ORGANIZER');
        if ($redirect = $this->requireStripeReady()) { // @codeCoverageIgnoreStart
            return $redirect;
        } // @codeCoverageIgnoreEnd

        $user = $this->requireEventOwnership($event);

        $billet = $em->getRepository(Billet::class)->find($billetId);
        if (!$billet || $billet->getCategory()->getEvent()->getId() !== $event->getId()) {
            throw $this->createNotFoundException();
        }

        $billetName = $billet->getName();
        $billetDbId = $billet->getId();
        $this->deleteBilletFromStripe($billet, $user, $stripeService);

        $em->remove($billet);
        $em->flush();

        $audit->log('billet_deleted', 'Billet', $billetDbId, ['name' => $billetName, 'event' => $event->getTitle()]);

        $this->addFlash('success', 'Billet supprime avec succes.');

        return $this->redirectToRoute('app_account_edit_event', ['id' => $event->getId(), 'tab' => 'categories']);
    }

    #[Route('/mon-compte/evenement/{id}/billet/reorder', name: 'app_account_event_reorder_billets', methods: ['POST'])]
    public function reorderBillets(Event $event, Request $request, EntityManagerInterface $em): Response
    {
        $this->denyAccessUnlessGranted('ROLE_ORGANIZER');
        if ($redirect = $this->requireStripeReady()) { // @codeCoverageIgnoreStart
            return $redirect;
        } // @codeCoverageIgnoreEnd

        $this->requireEventOwnership($event);

        $order = json_decode($request->getContent(), true);
        if (\is_array($order)) {
            foreach ($order as $position => $billetId) {
                $billet = $em->getRepository(Billet::class)->find($billetId);
                if ($billet && $billet->getCategory()->getEvent()->getId() === $event->getId()) {
                    $billet->setPosition($position);
                }
            }
            $em->flush();
        }

        return $this->json(['success' => true]);
    }

    #[Route('/mon-compte/evenement/{id}/invitation', name: 'app_account_event_create_invitation', methods: ['POST'])]
    public function createInvitation(Event $event, Request $request, EntityManagerInterface $em, BilletOrderService $billetOrderService): Response
    {
        $this->denyAccessUnlessGranted('ROLE_ORGANIZER');
        if ($redirect = $this->requireStripeReady()) { // @codeCoverageIgnoreStart
            return $redirect;
        } // @codeCoverageIgnoreEnd

        $this->requireEventOwnership($event);

        $result = $this->buildInvitationOrder($event, $request, $em);

        if ($result instanceof Response) {
            return $result;
        }

        $order = $result;
        $em->persist($order);
        $em->flush();

        $billetOrderService->generateOrderTickets($order);

        $tickets = $em->getRepository(BilletOrder::class)->findBy(['billetBuyer' => $order]);
        foreach ($tickets as $ticket) {
            $ticket->setIsInvitation(true);
        }
        $em->flush();

        $billetOrderService->generateAndSendTickets($order);

        $this->addFlash('success', 'Invitation envoyee a '.$order->getEmail().'.');

        return $this->redirectToRoute('app_account_edit_event', ['id' => $event->getId(), 'tab' => 'invitations']);
    }

    #[Route('/mon-compte/evenement/{id}/accreditation', name: 'app_account_event_create_accreditation', methods: ['POST'])]
    public function createAccreditation(Event $event, Request $request, EntityManagerInterface $em, BilletOrderService $billetOrderService): Response
    {
        $this->denyAccessUnlessGranted('ROLE_ORGANIZER');
        if ($redirect = $this->requireStripeReady()) { // @codeCoverageIgnoreStart
            return $redirect;
        } // @codeCoverageIgnoreEnd

        $this->requireEventOwnership($event);

        $accreditationType = $request->request->getString('accreditation_type', 'staff');
        if (!\in_array($accreditationType, ['staff', 'exposant'], true)) {
            $accreditationType = 'staff';
        }

        $firstName = trim($request->request->getString('first_name'));
        $lastName = trim($request->request->getString('last_name'));
        $email = trim($request->request->getString('email'));
        $redirectResponse = $this->redirectToRoute('app_account_edit_event', ['id' => $event->getId(), 'tab' => 'invitations']);
        $categories = $em->getRepository(Category::class)->findBy(['event' => $event], ['position' => 'ASC']);

        $missingFields = '' === $firstName || '' === $lastName || '' === $email;
        if ($missingFields || empty($categories)) {
            $this->addFlash('error', $missingFields ? 'Tous les champs sont requis.' : 'Creez au moins une categorie avant de generer une accreditation.');

            return $redirectResponse;
        }

        $category = $categories[0];
        $label = 'staff' === $accreditationType ? 'Staff' : 'Exposant';

        $billet = new Billet();
        $billet->setCategory($category);
        $billet->setName($label.' - '.$firstName.' '.$lastName);
        $billet->setType($accreditationType);
        $billet->setPriceHT(0);
        $billet->setQuantity(1);
        $billet->setIsGeneratedBillet(true);
        $billet->setNotBuyable(true);
        $billet->setPosition(9999);
        $em->persist($billet);
        $em->flush();

        $count = $em->getRepository(BilletBuyer::class)->count([]) + 1;

        $order = new BilletBuyer();
        $order->setEvent($event);
        $order->setFirstName($firstName);
        $order->setLastName($lastName);
        $order->setEmail($email);
        $order->setOrderNumber(date('Y-m-d').'-'.$count);
        $order->setTotalHT(0);
        $order->setIsInvitation(true);

        $item = new BilletBuyerItem();
        $item->setBillet($billet);
        $item->setBilletName($billet->getName());
        $item->setQuantity(1);
        $item->setUnitPriceHT(0);
        $order->addItem($item);

        $em->persist($order);
        $em->flush();

        $billetOrderService->generateOrderTickets($order);

        $tickets = $em->getRepository(BilletOrder::class)->findBy(['billetBuyer' => $order]);
        foreach ($tickets as $ticket) {
            $ticket->setIsInvitation(true);
        }
        $em->flush();

        $billetOrderService->generateAndSendTickets($order);

        $this->addFlash('success', 'Accreditation '.$label.' envoyee a '.$order->getEmail().'.');

        return $redirectResponse;
    }

    #[Route('/mon-compte/evenement/{id}/invitation/{orderId}/renvoyer', name: 'app_account_event_resend_invitation', methods: ['POST'])]
    public function resendInvitation(Event $event, int $orderId, EntityManagerInterface $em, BilletOrderService $billetOrderService): Response
    {
        $this->denyAccessUnlessGranted('ROLE_ORGANIZER');
        if ($redirect = $this->requireStripeReady()) { // @codeCoverageIgnoreStart
            return $redirect;
        } // @codeCoverageIgnoreEnd

        $this->requireEventOwnership($event);

        $order = $em->getRepository(BilletBuyer::class)->find($orderId);
        if (!$order || $order->getEvent()->getId() !== $event->getId()) {
            throw $this->createNotFoundException();
        }

        $billetOrderService->generateAndSendTickets($order);

        $this->addFlash('success', 'Invitation renvoyee a '.$order->getEmail().'.');

        return $this->redirectToRoute('app_account_edit_event', ['id' => $event->getId(), 'tab' => 'invitations']);
    }

    #[Route('/mon-compte/evenement/{id}/ticket/{ticketId}/telecharger', name: 'app_account_event_download_ticket', requirements: ['ticketId' => '\d+'], methods: ['GET'])]
    public function downloadTicket(Event $event, int $ticketId, EntityManagerInterface $em, BilletOrderService $billetOrderService): Response
    {
        $this->denyAccessUnlessGranted('ROLE_ORGANIZER');
        $this->requireEventOwnership($event);

        $ticket = $em->getRepository(BilletOrder::class)->find($ticketId);
        if (!$ticket || $ticket->getBilletBuyer()->getEvent()->getId() !== $event->getId()) {
            throw $this->createNotFoundException();
        }

        $pdf = $billetOrderService->generatePdf($ticket);

        return new Response($pdf, 200, [
            'Content-Type' => self::CONTENT_TYPE_PDF,
            'Content-Disposition' => 'inline; filename="'.$ticket->getReference().self::PDF_SUFFIX,
        ]);
    }

    #[Route('/mon-compte/evenement/{id}/ticket/{ticketId}/renvoyer', name: 'app_account_event_resend_ticket', requirements: ['ticketId' => '\d+'], methods: ['POST'])]
    public function resendTicket(Event $event, int $ticketId, EntityManagerInterface $em, BilletOrderService $billetOrderService): Response
    {
        $this->denyAccessUnlessGranted('ROLE_ORGANIZER');
        $this->requireEventOwnership($event);

        $ticket = $em->getRepository(BilletOrder::class)->find($ticketId);
        if (!$ticket || $ticket->getBilletBuyer()->getEvent()->getId() !== $event->getId()) {
            throw $this->createNotFoundException();
        }

        $billetOrderService->generateAndSendTickets($ticket->getBilletBuyer());

        $this->addFlash('success', 'Billet renvoye a '.$ticket->getBilletBuyer()->getEmail().'.');

        return $this->redirectToRoute('app_account_edit_event', ['id' => $event->getId(), 'tab' => 'tickets']);
    }

    #[Route('/mon-compte/evenement/{id}/ticket/{ticketId}/annuler', name: 'app_account_event_cancel_ticket', requirements: ['ticketId' => '\d+'], methods: ['POST'])]
    public function cancelTicket(Event $event, int $ticketId, EntityManagerInterface $em, AuditService $audit): Response
    {
        $this->denyAccessUnlessGranted('ROLE_ORGANIZER');
        $this->requireEventOwnership($event);

        $ticket = $em->getRepository(BilletOrder::class)->find($ticketId);
        if (!$ticket || $ticket->getBilletBuyer()->getEvent()->getId() !== $event->getId()) {
            throw $this->createNotFoundException();
        }

        $ticket->setState(BilletOrder::STATE_INVALID);
        $em->flush();

        $audit->log('ticket_cancelled', 'BilletOrder', $ticket->getId(), [
            'reference' => $ticket->getReference(),
            'event' => $event->getTitle(),
        ]);

        $this->addFlash('success', 'Billet '.$ticket->getReference().' annule.');

        return $this->redirectToRoute('app_account_edit_event', ['id' => $event->getId(), 'tab' => 'tickets']);
    }

    #[Route('/mon-compte/evenement/{id}/attestation', name: 'app_account_event_attestation', methods: ['POST'])]
    public function eventAttestation(Event $event, Request $request, EntityManagerInterface $em): Response
    {
        $this->denyAccessUnlessGranted('ROLE_ORGANIZER');
        $user = $this->requireEventOwnership($event);

        $categoryIds = array_map('intval', $request->request->all('categories'));
        $billetIds = array_map('intval', $request->request->all('billets'));

        $categories = $categoryIds ? $em->getRepository(Category::class)->findBy(['id' => $categoryIds, 'event' => $event]) : [];
        $billets = $billetIds ? array_filter($em->getRepository(Billet::class)->findBy(['id' => $billetIds]), fn (Billet $b) => $b->getCategory()->getEvent()->getId() === $event->getId()) : [];

        if (empty($categories) && empty($billets)) {
            $this->addFlash('error', 'Selectionnez au moins une categorie ou un billet.');

            return $this->redirectToRoute('app_account_edit_event', ['id' => $event->getId(), 'tab' => 'attestation']);
        }

        [$billets, $allBilletIds] = $this->collectAttestationBillets($billets, $categories, $em);
        [$billetLines, $totalSold] = $this->buildAttestationStats($billets, $allBilletIds, $em);
        $ticketDetails = $this->buildAttestationTicketDetails($allBilletIds, $em);

        $isSimple = 'simple' === $request->request->getString('mode', 'detail');
        $generatedAt = new \DateTimeImmutable();
        $selectedCategoryNames = array_map(fn ($c) => $c->getName(), $categories);

        $attestationData = [
            'ref' => 'ATT-'.$event->getId().'-'.$generatedAt->format('YmdHis'),
            'event' => $event->getTitle(),
            'eventDate' => $event->getStartAt()?->format('d/m/Y H:i').' - '.$event->getEndAt()?->format('d/m/Y H:i'),
            'eventLocation' => $event->getAddress().', '.$event->getZipcode().' '.$event->getCity(),
            'organizer' => $user->getCompanyName() ?? $user->getFirstName().' '.$user->getLastName(),
            'siret' => $user->getSiret(),
            'categories' => $selectedCategoryNames,
            'billets' => array_map(fn ($l) => ['cat' => $l['category'], 'name' => $l['name'], 'sold' => $l['sold']], $billetLines),
            'totalSold' => $totalSold,
            'generatedAt' => $generatedAt->format('d/m/Y H:i:s'),
        ];

        $signatureHash = hash_hmac('sha256', json_encode($attestationData, \JSON_UNESCAPED_UNICODE), $this->getParameter('kernel.secret'));

        $attestationEntity = new Attestation(
            $attestationData['ref'],
            $signatureHash,
            $event,
            $user,
            $totalSold,
            $attestationData,
        );
        $em->persist($attestationEntity);
        $em->flush();

        $verifyUrl = $this->generateUrl('app_attestation_ventes_ref', ['reference' => $attestationData['ref']], UrlGeneratorInterface::ABSOLUTE_URL);

        $qrCode = (new \Endroid\QrCode\Builder\Builder(
            writer: new \Endroid\QrCode\Writer\PngWriter(),
            data: $verifyUrl,
            encoding: new \Endroid\QrCode\Encoding\Encoding('UTF-8'),
            size: 300,
            margin: 5,
        ))->build();
        $qrBase64 = 'data:image/png;base64,'.base64_encode($qrCode->getString());

        $html = $this->renderView('pdf/attestation_ventes.html.twig', [
            'event' => $event,
            'organizer' => $user,
            'billetLines' => $billetLines,
            'totalSold' => $totalSold,
            'generatedAt' => $generatedAt,
            'selectedCategories' => $selectedCategoryNames,
            'verifyUrl' => $verifyUrl,
            'qrBase64' => $qrBase64,
            'attestationRef' => $attestationData['ref'],
            'signatureHash' => $signatureHash,
            'isSimple' => $isSimple,
            'ticketDetails' => $ticketDetails,
        ]);

        $dompdf = new \Dompdf\Dompdf();
        $dompdf->loadHtml($html);
        $dompdf->setPaper('A4');
        $dompdf->render();

        return new Response($dompdf->output(), 200, [
            'Content-Type' => self::CONTENT_TYPE_PDF,
            'Content-Disposition' => 'inline; filename="attestation_'.$event->getSlug().'_'.date('Y-m-d').self::PDF_SUFFIX,
        ]);
    }

    #[Route('/mon-compte/evenement/{id}/commande/{orderId}/annuler', name: 'app_account_event_cancel_order', methods: ['POST'])]
    public function cancelOrder(Event $event, int $orderId, EntityManagerInterface $em, AuditService $audit, BilletOrderService $billetOrderService): Response
    {
        $this->denyAccessUnlessGranted('ROLE_ORGANIZER');
        if ($redirect = $this->requireStripeReady()) { // @codeCoverageIgnoreStart
            return $redirect;
        } // @codeCoverageIgnoreEnd

        $this->requireEventOwnership($event);

        $order = $em->getRepository(BilletBuyer::class)->find($orderId);
        if (!$order || $order->getEvent()->getId() !== $event->getId()) {
            throw $this->createNotFoundException();
        }

        $order->setStatus(BilletBuyer::STATUS_CANCELLED);

        $tickets = $em->getRepository(BilletOrder::class)->findBy(['billetBuyer' => $order]);
        foreach ($tickets as $ticket) {
            $ticket->setState(BilletOrder::STATE_INVALID);
        }

        $em->flush();

        $audit->log('order_cancelled', 'BilletBuyer', $order->getId(), [
            'orderNumber' => $order->getOrderNumber(),
            'event' => $event->getTitle(),
        ]);

        $billetOrderService->notifyOrganizerCancelled($order, 'annulee');

        $this->addFlash('success', 'Commande '.$order->getOrderNumber().' annulee.');

        return $this->redirectToRoute('app_account_edit_event', ['id' => $event->getId(), 'tab' => 'stats']);
    }

    /**
     * @codeCoverageIgnore Requires live Stripe API
     */
    #[Route('/mon-compte/evenement/{id}/commande/{orderId}/rembourser', name: 'app_account_event_refund_order', methods: ['POST'])]
    public function refundOrder(Event $event, int $orderId, EntityManagerInterface $em, StripeService $stripeService, AuditService $audit, BilletOrderService $billetOrderService): Response
    {
        $this->denyAccessUnlessGranted('ROLE_ORGANIZER');
        if ($redirect = $this->requireStripeReady()) { // @codeCoverageIgnoreStart
            return $redirect;
        } // @codeCoverageIgnoreEnd

        $user = $this->requireEventOwnership($event);

        $order = $em->getRepository(BilletBuyer::class)->find($orderId);
        if (!$order || $order->getEvent()->getId() !== $event->getId()) {
            throw $this->createNotFoundException();
        }

        if ($order->getStripeSessionId() && $user->getStripeAccountId()) {
            try {
                $stripeService->getClient()->refunds->create([
                    'payment_intent' => $order->getStripeSessionId(),
                ], ['stripe_account' => $user->getStripeAccountId()]);
            } catch (\Exception) {
                // Stripe failure is non-blocking
            }
        }

        $order->setStatus(BilletBuyer::STATUS_REFUNDED);

        $tickets = $em->getRepository(BilletOrder::class)->findBy(['billetBuyer' => $order]);
        foreach ($tickets as $ticket) {
            $ticket->setState(BilletOrder::STATE_INVALID);
        }

        $em->flush();

        $audit->log('order_refunded', 'BilletBuyer', $order->getId(), [
            'orderNumber' => $order->getOrderNumber(),
            'event' => $event->getTitle(),
            'totalHT' => $order->getTotalHTDecimal(),
        ]);

        $billetOrderService->notifyOrganizerCancelled($order, 'remboursee');

        $this->addFlash('success', 'Commande '.$order->getOrderNumber().' remboursee.');

        return $this->redirectToRoute('app_account_edit_event', ['id' => $event->getId(), 'tab' => 'stats']);
    }

    #[Route('/mon-compte/evenement/{id}/billet-preview', name: 'app_account_event_billet_preview', methods: ['GET'])]
    public function billetPreview(Event $event, Request $request): Response
    {
        $this->denyAccessUnlessGranted('ROLE_ORGANIZER');
        if ($redirect = $this->requireStripeReady()) { // @codeCoverageIgnoreStart
            return $redirect;
        } // @codeCoverageIgnoreEnd

        $user = $this->requireEventOwnership($event);

        return $this->render('account/billet_preview.html.twig', [
            'event' => $event,
            'user' => $user,
            'bg_color' => '#ffffff',
            'text_color' => '#111111',
            'accent_color' => $request->query->getString('accent_color', '#4f46e5'),
            'show_logo' => true,
            'show_invitation' => true,
            'invitation_title' => $request->query->getString('invitation_title', 'Invitation'),
            'invitation_color' => $request->query->getString('invitation_color', '#d4a017'),
        ]);
    }

    #[Route('/mon-compte/evenement/{id}/billet-design', name: 'app_account_event_save_billet_design', methods: ['POST'])]
    public function saveBilletDesign(Event $event, Request $request, EntityManagerInterface $em): Response
    {
        $this->denyAccessUnlessGranted('ROLE_ORGANIZER');
        if ($redirect = $this->requireStripeReady()) { // @codeCoverageIgnoreStart
            return $redirect;
        } // @codeCoverageIgnoreEnd

        $this->requireEventOwnership($event);

        $design = $em->getRepository(BilletDesign::class)->findOneBy(['event' => $event]);
        if (!$design) {
            $design = new BilletDesign();
            $design->setEvent($event);
            $em->persist($design);
        }

        $design->setAccentColor($request->request->getString('accent_color', '#4f46e5'));
        $design->setInvitationTitle($request->request->getString('invitation_title', 'Invitation'));
        $design->setInvitationColor($request->request->getString('invitation_color', '#d4a017'));
        $design->setUpdatedAt(new \DateTimeImmutable());

        $em->flush();

        return $this->json(['success' => true]);
    }

    #[Route('/mon-compte/evenement/{id}/en-ligne', name: 'app_account_toggle_event_online', methods: ['POST'])]
    public function toggleEventOnline(Event $event, EntityManagerInterface $em, EventIndexService $eventIndex): Response
    {
        $this->denyAccessUnlessGranted('ROLE_ORGANIZER');
        if ($redirect = $this->requireStripeReady()) { // @codeCoverageIgnoreStart
            return $redirect;
        } // @codeCoverageIgnoreEnd

        $user = $this->requireEventOwnership($event);

        if (!$event->isOnline() && (!$user->isStripeChargesEnabled() || !$user->isStripePayoutsEnabled())) { // @codeCoverageIgnoreStart
            $this->addFlash('error', 'Configuration Stripe requise pour mettre un evenement en ligne.');

            return $this->redirectToRoute('app_account_edit_event', ['id' => $event->getId()]);
        } // @codeCoverageIgnoreEnd

        $event->setIsOnline(!$event->isOnline());
        $em->flush();

        $eventIndex->indexEvent($event);

        $this->addFlash('success', $event->isOnline() ? 'Evenement mis en ligne.' : 'Evenement passe hors ligne.');

        return $this->redirectToRoute('app_account_edit_event', ['id' => $event->getId()]);
    }

    #[Route('/mon-compte/evenement/{id}/secret', name: 'app_account_toggle_event_secret', methods: ['POST'])]
    public function toggleEventSecret(Event $event, EntityManagerInterface $em, EventIndexService $eventIndex): Response
    {
        $this->denyAccessUnlessGranted('ROLE_ORGANIZER');
        if ($redirect = $this->requireStripeReady()) { // @codeCoverageIgnoreStart
            return $redirect;
        } // @codeCoverageIgnoreEnd

        $this->requireEventOwnership($event);

        $event->setIsSecret(!$event->isSecret());
        $em->flush();

        $eventIndex->indexEvent($event);

        $this->addFlash('success', $event->isSecret() ? 'Evenement marque comme secret.' : 'Evenement rendu public.');

        return $this->redirectToRoute('app_account_edit_event', ['id' => $event->getId()]);
    }

    #[Route('/mon-compte/evenement/{id}/supprimer', name: 'app_account_delete_event', methods: ['POST'])]
    public function deleteEvent(Event $event, EntityManagerInterface $em, EventIndexService $eventIndex, AuditService $audit): Response
    {
        $this->denyAccessUnlessGranted('ROLE_ORGANIZER');
        if ($redirect = $this->requireStripeReady()) { // @codeCoverageIgnoreStart
            return $redirect;
        } // @codeCoverageIgnoreEnd

        $this->requireEventOwnership($event);

        $eventTitle = $event->getTitle();
        $eventDbId = $event->getId();
        $eventIndex->removeEvent($event);

        $em->remove($event);
        $em->flush();

        $audit->log('event_deleted', 'Event', $eventDbId, ['title' => $eventTitle]);

        $this->addFlash('success', 'Evenement supprime.');

        return $this->redirectToRoute('app_account', ['tab' => 'events']);
    }

    #[Route('/mon-compte/evenement/{id}/qrcode', name: 'app_account_event_qrcode', methods: ['GET'])]
    public function eventQrCode(Event $event, UrlGeneratorInterface $urlGenerator): Response
    {
        $this->denyAccessUnlessGranted('ROLE_ORGANIZER');
        if ($redirect = $this->requireStripeReady()) { // @codeCoverageIgnoreStart
            return $redirect;
        } // @codeCoverageIgnoreEnd

        $user = $this->requireEventOwnership($event);

        $eventUrl = $urlGenerator->generate('app_event_detail', [
            'orgaSlug' => $user->getSlug(),
            'id' => $event->getId(),
            'eventSlug' => $event->getSlug(),
        ], UrlGeneratorInterface::ABSOLUTE_URL);

        $qrCode = (new \Endroid\QrCode\Builder\Builder(
            writer: new \Endroid\QrCode\Writer\PngWriter(),
            data: $eventUrl,
            encoding: new \Endroid\QrCode\Encoding\Encoding('UTF-8'),
            size: 400,
            margin: 20,
        ))->build();

        return new Response($qrCode->getString(), 200, [
            'Content-Type' => 'image/png',
            'Content-Disposition' => 'attachment; filename="qrcode-'.$event->getSlug().'.png"',
        ]);
    }

    /** @codeCoverageIgnore Test helper, not used in production */
    #[Route('/mon-compte/test-payout', name: 'app_account_test_payout', methods: ['POST'])]
    public function testPayout(EntityManagerInterface $em): Response
    {
        /** @var User $user */
        $user = $this->getUser();

        if (!$this->isGranted('ROLE_ORGANIZER') || !$user->getStripeAccountId()) {
            return $this->redirectToRoute('app_account');
        }

        $payout = new Payout();
        $payout->setOrganizer($user);
        $payout->setStripePayoutId('po_test_'.bin2hex(random_bytes(8)));
        $payout->setStatus('paid');
        $payout->setAmount(random_int(1000, 50000));
        $payout->setCurrency('eur');
        $payout->setDestination('ba_test_bank');
        $payout->setStripeAccountId($user->getStripeAccountId());
        $payout->setArrivalDate(new \DateTimeImmutable('+2 days'));

        $em->persist($payout);
        $em->flush();

        $this->addFlash('success', sprintf('Payout test cree : %s (%.2f EUR)', $payout->getStripePayoutId(), $payout->getAmountDecimal()));

        return $this->redirectToRoute('app_account', ['tab' => 'payouts']);
    }

    /** @codeCoverageIgnore Requires live Stripe API */
    #[Route('/mon-compte/stripe-dashboard', name: 'app_account_stripe_dashboard')]
    public function stripeDashboard(StripeService $stripeService): Response
    {
        /** @var User $user */
        $user = $this->getUser();

        if (!$this->isGranted('ROLE_ORGANIZER') || !$user->getStripeAccountId()) {
            return $this->redirectToRoute('app_account');
        }

        try {
            $link = $stripeService->createLoginLink($user->getStripeAccountId());

            return $this->redirect($link);
        } catch (\Throwable $e) {
            $this->addFlash('error', 'Erreur Stripe : '.$e->getMessage());

            return $this->redirectToRoute('app_account');
        }
    }

    /** @codeCoverageIgnore Generates PDF with dompdf */
    #[Route('/mon-compte/payout/{id}/attestation', name: 'app_account_payout_pdf')]
    public function payoutPdf(Payout $payout, PayoutPdfService $pdfService): Response
    {
        /** @var User $user */
        $user = $this->getUser();

        if ($payout->getOrganizer()->getId() !== $user->getId()) {
            throw $this->createAccessDeniedException();
        }

        return new Response($pdfService->generate($payout), 200, [
            'Content-Type' => self::CONTENT_TYPE_PDF,
            'Content-Disposition' => 'inline; filename="attestation_'.$payout->getStripePayoutId().self::PDF_SUFFIX,
        ]);
    }

    private function buildInvitationOrder(Event $event, Request $request, EntityManagerInterface $em): BilletBuyer|Response
    {
        $firstName = trim($request->request->getString('first_name'));
        $lastName = trim($request->request->getString('last_name'));
        $email = trim($request->request->getString('email'));
        $items = $request->request->all('items');
        $redirect = $this->redirectToRoute('app_account_edit_event', ['id' => $event->getId(), 'tab' => 'invitations']);

        if ('' === $firstName || '' === $lastName || '' === $email || 0 === \count($items)) {
            $this->addFlash('error', 'Tous les champs sont requis.');

            return $redirect;
        }

        $count = $em->getRepository(BilletBuyer::class)->count([]) + 1;

        $order = new BilletBuyer();
        $order->setEvent($event);
        $order->setFirstName($firstName);
        $order->setLastName($lastName);
        $order->setEmail($email);
        $order->setOrderNumber(date('Y-m-d').'-'.$count);
        $order->setTotalHT(0);
        $order->setIsInvitation(true);

        foreach ($items as $itemData) {
            $billetId = (int) ($itemData['billet_id'] ?? 0);
            $qty = max(1, (int) ($itemData['quantity'] ?? 1));

            $billet = $em->getRepository(Billet::class)->find($billetId);
            if (!$billet || $billet->getCategory()->getEvent()->getId() !== $event->getId()) {
                continue;
            }

            $item = new BilletBuyerItem();
            $item->setBillet($billet);
            $item->setBilletName($billet->getName());
            $item->setQuantity($qty);
            $item->setUnitPriceHT(0);
            $order->addItem($item);
        }

        if ($order->getItems()->isEmpty()) {
            $this->addFlash('error', 'Aucun billet valide selectionne.');

            return $redirect;
        }

        return $order;
    }

    /**
     * @param list<Billet>   $billets
     * @param list<Category> $categories
     *
     * @return array{list<Billet>, list<int>}
     */
    private function collectAttestationBillets(array $billets, array $categories, EntityManagerInterface $em): array
    {
        $allBilletIds = array_map(fn (Billet $b) => $b->getId(), $billets);

        foreach ($categories as $cat) {
            foreach ($em->getRepository(Billet::class)->findBy(['category' => $cat]) as $b) {
                if (!\in_array($b->getId(), $allBilletIds, true)) {
                    $allBilletIds[] = $b->getId();
                    $billets[] = $b;
                }
            }
        }

        return [$billets, $allBilletIds];
    }

    /**
     * @param list<Billet> $billets
     * @param list<int>    $allBilletIds
     *
     * @return array{list<array<string, mixed>>, int}
     */
    private function buildAttestationStats(array $billets, array $allBilletIds, EntityManagerInterface $em): array
    {
        $soldCounts = [];
        if ($allBilletIds) {
            $rows = $em->createQueryBuilder()
                ->select('IDENTITY(bo.billet) AS billetId, COUNT(bo.id) AS cnt')
                ->from(BilletOrder::class, 'bo')
                ->join('bo.billetBuyer', 'bb')
                ->where('bo.billet IN (:ids)')
                ->andWhere(self::DQL_BB_EXCLUDE_INVITATIONS)
                ->setParameter('ids', $allBilletIds)
                ->groupBy('bo.billet')
                ->getQuery()
                ->getArrayResult();
            foreach ($rows as $row) {
                $soldCounts[(int) $row['billetId']] = (int) $row['cnt'];
            }
        }

        $billetLines = [];
        $totalSold = 0;
        foreach ($billets as $b) {
            $sold = $soldCounts[$b->getId()] ?? 0;
            $billetLines[] = [
                'category' => $b->getCategory()->getName(),
                'name' => $b->getName(),
                'sold' => $sold,
            ];
            $totalSold += $sold;
        }

        return [$billetLines, $totalSold];
    }

    /**
     * @param list<int> $allBilletIds
     *
     * @return list<array<string, mixed>>
     */
    private function buildAttestationTicketDetails(array $allBilletIds, EntityManagerInterface $em): array
    {
        if (!$allBilletIds) {
            return [];
        }

        $tickets = $em->createQueryBuilder()
            ->select('t', 'bb')
            ->from(BilletOrder::class, 't')
            ->join('t.billetBuyer', 'bb')
            ->where('t.billet IN (:ids)')
            ->andWhere(self::DQL_BB_EXCLUDE_INVITATIONS)
            ->setParameter('ids', $allBilletIds)
            ->orderBy('t.createdAt', 'ASC')
            ->getQuery()
            ->getResult();

        $details = [];
        foreach ($tickets as $t) {
            $details[] = [
                'reference' => $t->getReference(),
                'billetName' => $t->getBilletName(),
                'orderNumber' => $t->getBilletBuyer()->getOrderNumber(),
                'buyerName' => $t->getBilletBuyer()->getFirstName().' '.$t->getBilletBuyer()->getLastName(),
            ];
        }

        return $details;
    }

    private function requireEventOwnership(Event $event): User
    {
        /** @var User $user */
        $user = $this->getUser();
        if ($this->isGranted('ROLE_ROOT')) {
            return $event->getAccount();
        }
        if ($event->getAccount()->getId() !== $user->getId()) {
            throw $this->createAccessDeniedException();
        }

        return $user;
    }

    private function requireCategory(int $categoryId, Event $event, EntityManagerInterface $em): Category
    {
        $category = $em->getRepository(Category::class)->find($categoryId);
        if (!$category || $category->getEvent()->getId() !== $event->getId()) {
            throw $this->createNotFoundException();
        }

        return $category;
    }

    /** @codeCoverageIgnore Tested via testOrganizerWithoutStripeBlocksEventCreation */
    private function requireStripeReady(): ?Response
    {
        if ($this->isGranted('ROLE_ROOT')) {
            return null;
        }

        /** @var User $user */
        $user = $this->getUser();

        $stripeNotReady = !$user->isStripeChargesEnabled() || !$user->isStripePayoutsEnabled();
        $billingBlocked = $user->isBilling() && 'good' !== $user->getBillingState();

        return ($stripeNotReady || $billingBlocked) ? $this->redirectToRoute('app_account') : null;
    }

    /**
     * @codeCoverageIgnore Requires live Stripe API
     */
    private function deleteBilletFromStripe(Billet $billet, User $user, StripeService $stripeService): void
    {
        if ($billet->getStripeProductId() && $user->getStripeAccountId()) {
            try {
                $stripeService->deleteProduct($billet->getStripeProductId(), $user->getStripeAccountId());
            } catch (\Exception) {
                // Stripe failure is non-blocking
            }
        }
    }

    /**
     * @codeCoverageIgnore Requires live Stripe API
     */
    private function syncBilletToStripe(Billet $billet, User $user, StripeService $stripeService): void
    {
        if (!$user->getStripeAccountId()) {
            return;
        }

        try {
            if ($billet->getStripeProductId()) {
                $stripeService->updateProduct($billet, $user->getStripeAccountId());
            } else {
                $productId = $stripeService->createProduct($billet, $user->getStripeAccountId());
                $billet->setStripeProductId($productId);
            }
        } catch (\Exception) {
            // Stripe failure is non-blocking
        }
    }

    #[Route('/mon-compte/export/{year}/{month}', name: 'app_account_export', requirements: ['year' => '\d{4}', 'month' => '\d{1,2}'], methods: ['GET'])]
    public function export(int $year, int $month, ExportService $exportService): Response
    {
        $this->denyAccessUnlessGranted('ROLE_ORGANIZER');
        if ($redirect = $this->requireStripeReady()) { // @codeCoverageIgnoreStart
            return $redirect;
        } // @codeCoverageIgnoreEnd

        /** @var User $user */
        $user = $this->getUser();
        $stats = $exportService->getMonthlyStats($year, $month, $user);
        $csv = $exportService->generateCsv($stats['orders']);

        $filename = sprintf('export_%04d_%02d.csv', $year, $month);

        return new Response($csv, 200, [
            'Content-Type' => 'text/csv; charset=utf-8',
            'Content-Disposition' => 'attachment; filename="'.$filename.'"',
        ]);
    }

    #[Route('/mon-compte/export/{year}/{month}/pdf', name: 'app_account_export_pdf', requirements: ['year' => '\d{4}', 'month' => '\d{1,2}'], methods: ['GET'])]
    public function exportPdf(int $year, int $month, ExportService $exportService): Response
    {
        $this->denyAccessUnlessGranted('ROLE_ORGANIZER');
        if ($redirect = $this->requireStripeReady()) { // @codeCoverageIgnoreStart
            return $redirect;
        } // @codeCoverageIgnoreEnd

        /** @var User $user */
        $user = $this->getUser();
        $stats = $exportService->getMonthlyStats($year, $month, $user);
        $pdf = $exportService->generatePdf($stats, $year, $month, $user);

        $filename = sprintf('recap_%04d_%02d.pdf', $year, $month);

        return new Response($pdf, 200, [
            'Content-Type' => self::CONTENT_TYPE_PDF,
            'Content-Disposition' => 'inline; filename="'.$filename.'"',
        ]);
    }

    /**
     * @param list<BilletBuyer> $paidOrders
     *
     * @return array{totalHT: int, totalSold: int, billetStats: array<int, array{name: string, sold: int, revenue: int}>}
     */
    private function computeEventStats(array $paidOrders): array
    {
        $totalHT = 0;
        $totalSold = 0;
        $billetStats = [];

        foreach ($paidOrders as $order) {
            $totalHT += $order->getTotalHT();
            foreach ($order->getItems() as $item) {
                $totalSold += $item->getQuantity();
                $billetId = $item->getBillet()?->getId();
                if (!$billetId) { // @codeCoverageIgnore
                    continue; // @codeCoverageIgnore
                } // @codeCoverageIgnore
                if (!isset($billetStats[$billetId])) {
                    $billetStats[$billetId] = ['name' => $item->getBilletName(), 'sold' => 0, 'revenue' => 0];
                }
                $billetStats[$billetId]['sold'] += $item->getQuantity();
                $billetStats[$billetId]['revenue'] += $item->getLineTotalHT();
            }
        }

        return ['totalHT' => $totalHT, 'totalSold' => $totalSold, 'billetStats' => $billetStats];
    }

    private function hydrateEventFromRequest(Event $event, Request $request): void
    {
        $event->setTitle(trim($request->request->getString('title')));
        $event->setDescription(trim($request->request->getString('description')) ?: null);
        $event->setStartAt(new \DateTimeImmutable($request->request->getString('start_at')));
        $event->setEndAt(new \DateTimeImmutable($request->request->getString('end_at')));
        $event->setAddress(trim($request->request->getString('address')));
        $event->setZipcode(trim($request->request->getString('zipcode')));
        $event->setCity(trim($request->request->getString('city')));

        $pictureFile = $request->files->get('event_main_picture');
        if ($pictureFile) {
            $event->setEventMainPictureFile($pictureFile);
        }
    }

    /**
     * @return string[]
     */
    public static function getAllowedBilletTypes(?string $offer): array
    {
        return match ($offer) {
            'basic', 'sur-mesure' => ['billet', 'reservation_brocante', 'vote'],
            default => ['billet'],
        };
    }

    /**
     * @return array{paid: float, pending: float, refunded: float, cancelled: float, commissionEticket: float, commissionStripe: float, net: float}
     */
    private function computeFinanceStats(User $user, EntityManagerInterface $em): array
    {
        $stats = ['paid' => 0, 'pending' => 0, 'refunded' => 0, 'cancelled' => 0, 'commissionEticket' => 0, 'commissionStripe' => 0, 'net' => 0];

        $orgaOrders = $em->createQueryBuilder()
            ->select('o')
            ->from(BilletBuyer::class, 'o')
            ->join('o.event', 'e')
            ->where('e.account = :user')
            ->andWhere(self::DQL_EXCLUDE_INVITATIONS)
            ->setParameter('user', $user)
            ->getQuery()
            ->getResult();

        $rate = $user->getCommissionRate() ?? 3;
        $stripeFeeRate = (float) $this->getParameter('stripe_fee_rate');
        $stripeFeeFixed = (int) $this->getParameter('stripe_fee_fixed');

        $statusMap = [
            BilletBuyer::STATUS_PENDING => 'pending',
            BilletBuyer::STATUS_REFUNDED => 'refunded',
            BilletBuyer::STATUS_CANCELLED => 'cancelled',
        ];

        foreach ($orgaOrders as $o) {
            $ht = $o->getTotalHT() / 100;
            $status = $o->getStatus();

            if (BilletBuyer::STATUS_PAID === $status) {
                $stats['paid'] += $ht;
                $stats['commissionEticket'] += $ht * ($rate / 100);
                $stats['commissionStripe'] += $ht * $stripeFeeRate + $stripeFeeFixed / 100;
            } elseif (isset($statusMap[$status])) {
                $stats[$statusMap[$status]] += $ht;
            }
        }

        $stats['net'] = $stats['paid'] - $stats['commissionEticket'] - $stats['commissionStripe'];

        return $stats;
    }

    private function hydrateBilletFromRequest(Billet $billet, Request $request): void
    {
        $billet->setName(trim($request->request->getString('name')));
        $billet->setPriceHT((int) round((float) $request->request->getString('price_ht') * 100));
        $qty = $request->request->getString('quantity');
        $billet->setQuantity('' === $qty ? null : (int) $qty);
        $billet->setIsGeneratedBillet($request->request->getBoolean('is_generated_billet'));
        $billet->setHasDefinedExit($request->request->getBoolean('has_defined_exit'));
        $billet->setNotBuyable($request->request->getBoolean('not_buyable'));

        $type = $request->request->getString('type', 'billet');
        /** @var User $user */
        $user = $this->getUser();
        $allowedTypes = self::getAllowedBilletTypes($user->getOffer());
        if (!\in_array($type, $allowedTypes, true)) {
            $type = 'billet';
        }
        $billet->setType($type);

        $billet->setDescription(trim($request->request->getString('description')) ?: null);

        $pictureFile = $request->files->get('picture');
        if ($pictureFile) {
            $billet->setPictureFile($pictureFile);
        }
    }
}
