<?php

namespace App\Controller;

use App\Entity\Advert;
use App\Entity\AdvertPayment;
use App\Entity\Echeancier;
use App\Entity\EcheancierLine;
use App\Entity\Facture;
use App\Entity\StripeWebhookSecret;
use App\Repository\StripeWebhookSecretRepository;
use App\Service\DocuSealService;
use App\Service\FactureService;
use App\Service\MailerService;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\KernelInterface;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Twig\Environment;

class WebhookStripeController extends AbstractController
{
    private const NOTIFICATION_EMAIL = 'notification@e-cosplay.fr';
    private const LOG_PI_SUCCEEDED = 'Stripe payment_intent.succeeded [';

    public function __construct(
        private LoggerInterface $logger,
        private StripeWebhookSecretRepository $secretRepository,
        private EntityManagerInterface $em,
        private MailerService $mailer,
        private Environment $twig,
        private FactureService $factureService,
        private KernelInterface $kernel,
        private UrlGeneratorInterface $urlGenerator,
        private DocuSealService $docuSealService,
    ) {
    }

    #[Route('/webhooks/stripe/main/light', name: 'app_webhook_stripe_main_light', methods: ['POST'])]
    public function mainLight(Request $request): Response
    {
        return $this->handleWebhook($request, StripeWebhookSecret::TYPE_MAIN_LIGHT);
    }

    #[Route('/webhooks/stripe/main/instant', name: 'app_webhook_stripe_main_instant', methods: ['POST'])]
    public function mainInstant(Request $request): Response
    {
        return $this->handleWebhook($request, StripeWebhookSecret::TYPE_MAIN_INSTANT);
    }

    #[Route('/webhooks/stripe/connect/light', name: 'app_webhook_stripe_connect_light', methods: ['POST'])]
    public function connectLight(Request $request): Response
    {
        return $this->handleWebhook($request, StripeWebhookSecret::TYPE_CONNECT_LIGHT);
    }

    #[Route('/webhooks/stripe/connect/instant', name: 'app_webhook_stripe_connect_instant', methods: ['POST'])]
    public function connectInstant(Request $request): Response
    {
        return $this->handleWebhook($request, StripeWebhookSecret::TYPE_CONNECT_INSTANT);
    }

    /** @codeCoverageIgnore */
    private function handleWebhook(Request $request, string $channel): Response
    {
        $secret = $this->secretRepository->getSecret($channel);

        if (null === $secret) {
            $this->logger->warning('Stripe webhook ['.$channel.']: secret non configure');

            return new JsonResponse(['error' => 'Webhook not configured'], Response::HTTP_SERVICE_UNAVAILABLE);
        }

        try {
            $event = \Stripe\Webhook::constructEvent($request->getContent(), $request->headers->get('Stripe-Signature', ''), $secret);
        } catch (\Throwable $e) {
            $msg = $e instanceof \Stripe\Exception\SignatureVerificationException ? 'Invalid signature' : 'Invalid payload';

            return new JsonResponse(['error' => $msg], Response::HTTP_BAD_REQUEST);
        }

        $this->logger->info('Stripe webhook ['.$channel.']: '.$event->type, [
            'event_id' => $event->id,
            'channel' => $channel,
            'type' => $event->type,
        ]);

        // Dispatch par type d'evenement (main + connect)
        return match ($event->type) {
            'payment_intent.succeeded' => $this->handlePaymentSucceeded($event, $channel),
            'payment_intent.payment_failed' => $this->handlePaymentFailed($event, $channel),
            'invoice.paid' => $this->handleInvoicePaid($event, $channel),
            'invoice.payment_failed' => $this->handleInvoiceFailed($event, $channel),
            default => new JsonResponse(['status' => 'ok', 'channel' => $channel, 'event' => $event->type]),
        };
    }

    /**
     * @codeCoverageIgnore
     */
    private function handlePaymentSucceeded(\Stripe\Event $event, string $channel): JsonResponse
    {
        $paymentIntent = $event->data->object;
        /** @phpstan-ignore-next-line */
        $metadata = $paymentIntent->metadata instanceof \Stripe\StripeObject ? $paymentIntent->metadata->toArray() : (array) ($paymentIntent->metadata ?? []);

        // Gestion echeancier SEPA
        $echeancierId = $metadata['echeancier_id'] ?? null;
        $lineId = $metadata['echeancier_line_id'] ?? null;
        if (null !== $echeancierId && null !== $lineId) {
            return $this->handleEcheancierPaymentSucceeded($paymentIntent, (int) $echeancierId, (int) $lineId, $channel);
        }

        // Gestion E-Flex
        $eflexId = $metadata['eflex_id'] ?? null;
        $eflexLineId = $metadata['eflex_line_id'] ?? null;
        if (null !== $eflexId && null !== $eflexLineId) {
            return $this->handleEFlexPaymentSucceeded($paymentIntent, (int) $eflexId, (int) $eflexLineId, $channel);
        }

        $advertId = $metadata['advert_id'] ?? null;
        $advert = null !== $advertId ? $this->em->getRepository(Advert::class)->find((int) $advertId) : null;

        $earlyResult = $this->checkPaymentPreConditions($advert, $advertId, $paymentIntent, $channel, $metadata);
        if (null !== $earlyResult) {
            return $earlyResult;
        }

        /** @phpstan-ignore-next-line */
        $amount = number_format($paymentIntent->amount_received / 100, 2, '.', '');
        $method = $metadata['payment_method'] ?? ($paymentIntent->payment_method_types[0] ?? null);
        $isConnect = str_contains($channel, 'connect');
        $revendeurCode = $metadata['revendeur_code'] ?? null;

        // Determiner le label methode pour les mails
        $methodLabel = match ($method) {
            'sepa_debit' => 'Prelevement SEPA',
            'customer_balance' => 'Virement bancaire (Stripe)',
            'paypal' => 'PayPal',
            'klarna' => 'Klarna',
            'revolut_pay' => 'Revolut Pay',
            'amazon_pay' => 'Amazon Pay',
            'link' => 'Link (Stripe)',
            default => 'Carte bancaire',
        };

        // Creer l'AdvertPayment
        $payment = new AdvertPayment($advert, AdvertPayment::TYPE_SUCCESS, $amount);
        $payment->setMethod($method);
        $this->em->persist($payment);

        // Mettre a jour l'etat de l'avis
        $advert->setState(Advert::STATE_ACCEPTED);
        $advert->setStripePaymentId($paymentIntent->id);

        // Tracker l'evenement paiement
        $this->em->persist(new \App\Entity\AdvertEvent($advert, \App\Entity\AdvertEvent::TYPE_PAY, $methodLabel.' - '.$amount.' EUR'));
        $this->em->flush();

        // Generation automatique de la facture si montant paye == montant avis
        $facture = null;
        try {
            $facture = $this->factureService->createPaidFactureFromAdvert($advert, $amount, $methodLabel);

            // Generer le PDF + envoyer au client
            if (null !== $facture) {
                $this->generateAndSendFacture($facture);
            }
        } catch (\Throwable $e) {
            $this->logger->error('Stripe webhook: erreur generation facture: '.$e->getMessage());
        }

        $customer = $advert->getCustomer();
        $numOrder = $advert->getOrderNumber()->getNumOrder();

        // Mail client : confirmation paiement
        if (null !== $customer && null !== $customer->getEmail()) {
            try {
                $this->mailer->sendEmail(
                    $customer->getEmail(),
                    'Paiement confirme - Avis '.$numOrder,
                    $this->twig->render('emails/payment_stripe_success_client.html.twig', [
                        'customer' => $customer,
                        'advert' => $advert,
                        'amount' => $amount,
                        'methodLabel' => $methodLabel,
                    ]),
                    null,
                    null,
                    false,
                );
            } catch (\Throwable $e) {
                $this->logger->error('Stripe webhook: erreur envoi mail client: '.$e->getMessage());
            }
        }

        // Notification admin
        $adminSubject = ($isConnect ? '[Connect] ' : '').'Paiement recu ('.$methodLabel.') - '.$numOrder.' - '.($customer?->getFullName() ?? 'Inconnu');
        if (null !== $revendeurCode) {
            $adminSubject .= ' [Revendeur '.$revendeurCode.']';
        }

        try {
            $this->mailer->sendEmail(
                self::NOTIFICATION_EMAIL,
                $adminSubject,
                $this->twig->render('emails/payment_notification_admin.html.twig', [
                    'customer' => $customer,
                    'advert' => $advert,
                    'method' => $methodLabel.' - '.$amount.' EUR'.($isConnect && null !== $revendeurCode ? ' (via revendeur '.$revendeurCode.')' : ''),
                ]),
                null,
                null,
                false,
            );
        } catch (\Throwable $e) {
            $this->logger->error('Stripe webhook: erreur envoi mail admin: '.$e->getMessage());
        }

        $this->logger->info(self::LOG_PI_SUCCEEDED.$channel.']: advert '.$numOrder.' -> accepted, '.$amount.' EUR, methode '.$method.($isConnect ? ' (connect)' : ''));

        return new JsonResponse(['status' => 'ok', 'action' => 'payment_accepted', 'advert' => $numOrder, 'amount' => $amount, 'method' => $method, 'channel' => $channel]);
    }

    /**
     * @codeCoverageIgnore
     *
     * @param array<string, mixed> $metadata
     */
    private function checkPaymentPreConditions(?Advert $advert, ?string $advertId, object $paymentIntent, string $channel, array $metadata): ?JsonResponse
    {
        if (null === $advertId || null === $advert) {
            $action = null === $advertId ? 'no_advert' : 'advert_not_found';
            $logMethod = null === $advertId ? 'info' : 'warning';
            $this->logger->$logMethod(self::LOG_PI_SUCCEEDED.$channel.']: '.(null === $advertId ? 'pas de advert_id dans metadata' : 'advert introuvable'), null === $advertId ? ['metadata' => $metadata] : ['advert_id' => $advertId]);

            return new JsonResponse(['status' => 'ok', 'action' => $action]);
        }

        if (Advert::STATE_ACCEPTED === $advert->getState() && $advert->getStripePaymentId() === $paymentIntent->id) {
            $this->logger->info(self::LOG_PI_SUCCEEDED.$channel.']: deja traite', ['pi' => $paymentIntent->id]);

            return new JsonResponse(['status' => 'ok', 'action' => 'already_processed']);
        }

        return null;
    }

    /**
     * @codeCoverageIgnore
     */
    private function handlePaymentFailed(\Stripe\Event $event, string $channel): JsonResponse
    {
        $paymentIntent = $event->data->object;
        /** @phpstan-ignore-next-line */
        $metadata = $paymentIntent->metadata instanceof \Stripe\StripeObject ? $paymentIntent->metadata->toArray() : (array) ($paymentIntent->metadata ?? []);

        // Gestion echeancier SEPA
        $echeancierId = $metadata['echeancier_id'] ?? null;
        $lineId = $metadata['echeancier_line_id'] ?? null;
        if (null !== $echeancierId && null !== $lineId) {
            return $this->handleEcheancierPaymentFailed($paymentIntent, (int) $echeancierId, (int) $lineId, $channel);
        }

        // Gestion E-Flex
        $eflexId = $metadata['eflex_id'] ?? null;
        $eflexLineId = $metadata['eflex_line_id'] ?? null;
        if (null !== $eflexId && null !== $eflexLineId) {
            return $this->handleEFlexPaymentFailed($paymentIntent, (int) $eflexId, (int) $eflexLineId, $channel);
        }

        $advertId = $metadata['advert_id'] ?? null;

        if (null === $advertId) {
            return new JsonResponse(['status' => 'ok', 'action' => 'no_advert']);
        }

        $advert = $this->em->getRepository(Advert::class)->find((int) $advertId);
        if (null === $advert) {
            return new JsonResponse(['status' => 'ok', 'action' => 'advert_not_found']);
        }

        $amount = number_format(($paymentIntent->amount ?? 0) / 100, 2, '.', '');
        $method = $metadata['payment_method'] ?? ($paymentIntent->payment_method_types[0] ?? null);
        $errorMessage = $paymentIntent->last_payment_error->message ?? 'Paiement refuse';

        $methodLabel = match ($method) {
            'sepa_debit' => 'Prelevement SEPA',
            'customer_balance' => 'Virement bancaire (Stripe)',
            'paypal' => 'PayPal',
            default => 'Carte bancaire',
        };

        // Creer l'AdvertPayment refused
        $payment = new AdvertPayment($advert, AdvertPayment::TYPE_REFUSED, $amount);
        $payment->setMethod($method);
        $this->em->persist($payment);
        $this->em->flush();

        $customer = $advert->getCustomer();
        $numOrder = $advert->getOrderNumber()->getNumOrder();
        $revendeurCode = $metadata['revendeur_code'] ?? null;
        $isConnect = str_contains($channel, 'connect');

        // Mail client : notification du refus de paiement
        if (null !== $customer && null !== $customer->getEmail()) {
            try {
                $this->mailer->sendEmail(
                    $customer->getEmail(),
                    'Paiement refuse - Avis '.$numOrder,
                    $this->twig->render('emails/payment_stripe_failed_client.html.twig', [
                        'customer' => $customer,
                        'advert' => $advert,
                        'amount' => $amount,
                        'methodLabel' => $methodLabel,
                        'errorMessage' => $errorMessage,
                    ]),
                    null,
                    null,
                    false,
                );
            } catch (\Throwable $e) {
                $this->logger->error('Stripe webhook: erreur envoi mail client echec: '.$e->getMessage());
            }
        }

        // Notification admin
        try {
            $this->mailer->sendEmail(
                self::NOTIFICATION_EMAIL,
                ($isConnect ? '[Connect] ' : '').'Paiement echoue ('.$methodLabel.') - '.$numOrder.' - '.($customer?->getFullName() ?? 'Inconnu').($revendeurCode ? ' [Revendeur '.$revendeurCode.']' : ''),
                $this->twig->render('emails/payment_notification_admin.html.twig', [
                    'customer' => $customer,
                    'advert' => $advert,
                    'method' => $methodLabel.' refuse - '.$errorMessage,
                ]),
                null,
                null,
                false,
            );
        } catch (\Throwable $e) {
            $this->logger->error('Stripe webhook: erreur envoi mail admin echec: '.$e->getMessage());
        }

        $this->logger->warning('Stripe payment_intent.payment_failed ['.$channel.']: advert '.$numOrder.' - '.$methodLabel.' - '.$errorMessage);

        return new JsonResponse(['status' => 'ok', 'action' => 'payment_failed', 'advert' => $numOrder, 'method' => $method]);
    }

    /**
     * Traite une invoice Stripe payee (echeancier).
     *
     * @codeCoverageIgnore
     */
    private function handleInvoicePaid(\Stripe\Event $event, string $channel): JsonResponse
    {
        $invoice = $event->data->object;
        $subscriptionId = $invoice->subscription ?? null;

        if (null === $subscriptionId) {
            return new JsonResponse(['status' => 'ok', 'action' => 'no_subscription']);
        }

        $echeancier = $this->em->getRepository(Echeancier::class)->findOneBy(['stripeSubscriptionId' => $subscriptionId]);
        if (null === $echeancier) {
            $this->logger->info('Stripe invoice.paid ['.$channel.']: echeancier non trouve pour subscription '.$subscriptionId);

            return new JsonResponse(['status' => 'ok', 'action' => 'echeancier_not_found']);
        }

        // Trouver la prochaine ligne en attente
        $nextLine = null;
        foreach ($echeancier->getLines() as $line) {
            if (EcheancierLine::STATE_PREPARED === $line->getState()) {
                $nextLine = $line;
                break;
            }
        }

        if (null === $nextLine) {
            $this->logger->warning('Stripe invoice.paid ['.$channel.']: aucune ligne en attente pour echeancier '.$echeancier->getId());

            return new JsonResponse(['status' => 'ok', 'action' => 'no_pending_line']);
        }

        $nextLine->setState(EcheancierLine::STATE_OK);
        $nextLine->setPaidAt(new \DateTimeImmutable());
        $nextLine->setStripeInvoiceId($invoice->id);
        $this->em->flush();

        // Verifier si toutes les echeances sont payees
        if ($echeancier->getNbPaid() >= $echeancier->getNbLines()) {
            $echeancier->setState(Echeancier::STATE_COMPLETED);
            $this->em->flush();
            $this->logger->info('Stripe invoice.paid ['.$channel.']: echeancier '.$echeancier->getId().' termine');
        }

        // Notification client
        $customer = $echeancier->getCustomer();
        if (null !== $customer->getEmail()) {
            try {
                $amount = number_format((float) $nextLine->getAmount(), 2, ',', ' ');
                $this->mailer->sendEmail(
                    $customer->getEmail(),
                    'Echeance '.$nextLine->getPosition().'/'.$echeancier->getNbLines().' payee - '.$amount.' EUR',
                    $this->twig->render('emails/echeancier_echeance_payee.html.twig', [
                        'customer' => $customer,
                        'echeancier' => $echeancier,
                        'line' => $nextLine,
                    ]),
                    null,
                    null,
                    false,
                );
            } catch (\Throwable $e) {
                $this->logger->error('Stripe invoice.paid: erreur envoi mail: '.$e->getMessage());
            }
        }

        $this->logger->info('Stripe invoice.paid ['.$channel.']: echeance '.$nextLine->getPosition().' payee pour echeancier '.$echeancier->getId());

        return new JsonResponse(['status' => 'ok', 'action' => 'echeance_paid', 'position' => $nextLine->getPosition()]);
    }

    /**
     * Traite un echec de paiement d'invoice Stripe (echeancier).
     *
     * @codeCoverageIgnore
     */
    private function handleInvoiceFailed(\Stripe\Event $event, string $channel): JsonResponse
    {
        $invoice = $event->data->object;
        $subscriptionId = $invoice->subscription ?? null;

        if (null === $subscriptionId) {
            return new JsonResponse(['status' => 'ok', 'action' => 'no_subscription']);
        }

        $echeancier = $this->em->getRepository(Echeancier::class)->findOneBy(['stripeSubscriptionId' => $subscriptionId]);
        if (null === $echeancier) {
            return new JsonResponse(['status' => 'ok', 'action' => 'echeancier_not_found']);
        }

        // Trouver la prochaine ligne en attente
        $nextLine = null;
        foreach ($echeancier->getLines() as $line) {
            if (EcheancierLine::STATE_PREPARED === $line->getState()) {
                $nextLine = $line;
                break;
            }
        }

        if (null === $nextLine) {
            return new JsonResponse(['status' => 'ok', 'action' => 'no_pending_line']);
        }

        $errorMessage = $invoice->last_finalization_error->message ?? ($invoice->status_transitions->finalized_at ? 'Paiement refuse' : 'Echec prelevement');

        $nextLine->setState(EcheancierLine::STATE_KO);
        $nextLine->setFailureReason($errorMessage);
        $nextLine->setStripeInvoiceId($invoice->id);

        // Si trop d'echecs, passer en defaut
        if ($echeancier->getNbFailed() >= 2) {
            $echeancier->setState(Echeancier::STATE_DEFAULT);
        }

        $this->em->flush();

        // Notification client + admin
        $customer = $echeancier->getCustomer();
        if (null !== $customer->getEmail()) {
            try {
                $this->mailer->sendEmail(
                    $customer->getEmail(),
                    'Echec prelevement echeance '.$nextLine->getPosition().'/'.$echeancier->getNbLines(),
                    $this->twig->render('emails/echeancier_echeance_echec.html.twig', [
                        'customer' => $customer,
                        'echeancier' => $echeancier,
                        'line' => $nextLine,
                        'errorMessage' => $errorMessage,
                    ]),
                    null,
                    null,
                    false,
                );
            } catch (\Throwable $e) {
                $this->logger->error('Stripe invoice.payment_failed: erreur envoi mail: '.$e->getMessage());
            }
        }

        // Notification admin
        try {
            $this->mailer->sendEmail(
                self::NOTIFICATION_EMAIL,
                'Echec echeance '.$nextLine->getPosition().' - '.$customer->getFullName(),
                $this->twig->render('emails/echeancier_echeance_echec.html.twig', [
                    'customer' => $customer,
                    'echeancier' => $echeancier,
                    'line' => $nextLine,
                    'errorMessage' => $errorMessage,
                ]),
                null,
                null,
                false,
            );
        } catch (\Throwable $e) {
            $this->logger->error('Stripe invoice.payment_failed: erreur envoi mail admin: '.$e->getMessage());
        }

        $this->logger->warning('Stripe invoice.payment_failed ['.$channel.']: echeance '.$nextLine->getPosition().' echouee pour echeancier '.$echeancier->getId().' - '.$errorMessage);

        return new JsonResponse(['status' => 'ok', 'action' => 'echeance_failed', 'position' => $nextLine->getPosition()]);
    }

    /**
     * Traite un PaymentIntent reussi pour une echeance SEPA.
     *
     * @codeCoverageIgnore
     */
    private function handleEcheancierPaymentSucceeded(object $paymentIntent, int $echeancierId, int $lineId, string $channel): JsonResponse
    {
        $line = $this->em->getRepository(EcheancierLine::class)->find($lineId);
        if (null === $line || $line->getEcheancier()->getId() !== $echeancierId) {
            return new JsonResponse(['status' => 'ok', 'action' => 'line_not_found']);
        }

        if (EcheancierLine::STATE_OK === $line->getState()) {
            return new JsonResponse(['status' => 'ok', 'action' => 'already_paid']);
        }

        $echeancier = $line->getEcheancier();
        $line->setState(EcheancierLine::STATE_OK);
        $line->setPaidAt(new \DateTimeImmutable());
        $line->setStripePaymentIntentId($paymentIntent->id);
        $this->em->flush();

        // Creer l'AdvertPayment si un avis est lie
        $this->createAdvertPaymentForLine($echeancier, $line);

        // Verifier si toutes les echeances sont payees
        if ($echeancier->getNbPaid() >= $echeancier->getNbLines()) {
            $echeancier->setState(Echeancier::STATE_COMPLETED);
            $this->em->flush();

            // Passer l'avis en accepted si lie
            $this->markAdvertAsAccepted($echeancier);

            // Envoyer l'attestation de fin de paiement
            $this->sendEcheancierCompletedNotifications($echeancier);
        }

        // Notification client echeance payee
        $customer = $echeancier->getCustomer();
        if (null !== $customer->getEmail()) {
            try {
                $this->mailer->sendEmail(
                    $customer->getEmail(),
                    'Echeance '.$line->getPosition().'/'.$echeancier->getNbLines().' payee - '.$echeancier->getReference(),
                    $this->twig->render('emails/echeancier_echeance_payee.html.twig', [
                        'customer' => $customer,
                        'echeancier' => $echeancier,
                        'line' => $line,
                    ]),
                    null,
                    null,
                    false,
                );
            } catch (\Throwable $e) {
                $this->logger->error('Stripe echeancier PI succeeded: erreur envoi mail: '.$e->getMessage());
            }
        }

        $this->logger->info('Stripe PI succeeded ['.$channel.']: echeance '.$line->getPosition().' payee pour '.$echeancier->getReference());

        return new JsonResponse(['status' => 'ok', 'action' => 'echeance_paid', 'position' => $line->getPosition(), 'reference' => $echeancier->getReference()]);
    }

    /**
     * Traite un PaymentIntent echoue pour une echeance SEPA.
     *
     * @codeCoverageIgnore
     */
    private function handleEcheancierPaymentFailed(object $paymentIntent, int $echeancierId, int $lineId, string $channel): JsonResponse
    {
        $line = $this->em->getRepository(EcheancierLine::class)->find($lineId);
        if (null === $line || $line->getEcheancier()->getId() !== $echeancierId) {
            return new JsonResponse(['status' => 'ok', 'action' => 'line_not_found']);
        }

        $echeancier = $line->getEcheancier();
        $errorMessage = $paymentIntent->last_payment_error->message ?? 'Echec prelevement SEPA';

        $line->setState(EcheancierLine::STATE_KO);
        $line->setFailureReason($errorMessage);
        $line->setStripePaymentIntentId($paymentIntent->id);
        $this->em->flush();

        // Si 2 echecs ou plus : annuler l'echeancier
        if ($echeancier->getNbFailed() >= 2) {
            $echeancier->setState(Echeancier::STATE_CANCELLED);
            $this->em->flush();

            // Inserer les paiements deja recus dans l'avis lie
            $this->syncPaidLinesToAdvert($echeancier);

            // Notifications annulation
            $this->sendEcheancierCancelledAfterRejectsNotifications($echeancier);
        }

        // Notification client avec lien de regularisation CB
        $customer = $echeancier->getCustomer();
        $regularizeUrl = $this->urlGenerator->generate('app_echeancier_regularize', [
            'id' => $echeancier->getId(),
            'lineId' => $line->getId(),
        ], UrlGeneratorInterface::ABSOLUTE_URL);

        if (null !== $customer->getEmail()) {
            try {
                $this->mailer->sendEmail(
                    $customer->getEmail(),
                    'Echec prelevement echeance '.$line->getPosition().'/'.$echeancier->getNbLines().' - '.$echeancier->getReference(),
                    $this->twig->render('emails/echeancier_echeance_echec.html.twig', [
                        'customer' => $customer,
                        'echeancier' => $echeancier,
                        'line' => $line,
                        'errorMessage' => $errorMessage,
                        'regularizeUrl' => $regularizeUrl,
                    ]),
                    null,
                    null,
                    false,
                );
            } catch (\Throwable $e) {
                $this->logger->error('Stripe echeancier PI failed: erreur envoi mail client: '.$e->getMessage());
            }
        }

        // Notification admin
        try {
            $this->mailer->sendEmail(
                self::NOTIFICATION_EMAIL,
                'Echec echeance '.$line->getPosition().' - '.$echeancier->getReference().' - '.$customer->getFullName(),
                $this->twig->render('emails/echeancier_echeance_echec.html.twig', [
                    'customer' => $customer,
                    'echeancier' => $echeancier,
                    'line' => $line,
                    'errorMessage' => $errorMessage,
                ]),
                null,
                null,
                false,
            );
        } catch (\Throwable $e) {
            $this->logger->error('Stripe echeancier PI failed: erreur envoi mail admin: '.$e->getMessage());
        }

        $this->logger->warning('Stripe PI failed ['.$channel.']: echeance '.$line->getPosition().' echouee pour '.$echeancier->getReference().' - '.$errorMessage);

        return new JsonResponse(['status' => 'ok', 'action' => 'echeance_failed', 'position' => $line->getPosition(), 'reference' => $echeancier->getReference()]);
    }

    /**
     * @codeCoverageIgnore
     */
    private function handleEFlexPaymentSucceeded(object $paymentIntent, int $eflexId, int $lineId, string $channel): JsonResponse
    {
        $line = $this->em->getRepository(\App\Entity\EFlexLine::class)->find($lineId);
        if (null === $line || $line->getEflex()->getId() !== $eflexId) {
            return new JsonResponse(['status' => 'ok', 'action' => 'eflex_line_not_found']);
        }

        if (\App\Entity\EFlexLine::STATE_OK === $line->getState()) {
            return new JsonResponse(['status' => 'ok', 'action' => 'already_paid']);
        }

        $eflex = $line->getEflex();
        $line->setState(\App\Entity\EFlexLine::STATE_OK);
        $line->setPaidAt(new \DateTimeImmutable());
        $line->setStripePaymentIntentId($paymentIntent->id);
        $line->setPaidMethod('stripe');

        // Sauvegarder le PaymentMethod pour les prelevements futurs (si pas deja fait)
        if (null === $eflex->getStripePaymentMethodId()) {
            $pmId = $paymentIntent->payment_method ?? null;
            if (null !== $pmId) {
                $eflex->setStripePaymentMethodId((string) $pmId);
                $eflex->setState(\App\Entity\EFlex::STATE_ACTIVE);
            }
        }

        $this->em->flush();

        if ($eflex->getNbPaid() >= $eflex->getNbLines()) {
            $eflex->setState(\App\Entity\EFlex::STATE_COMPLETED);
            $this->em->flush();
        }

        $customer = $eflex->getCustomer();
        if (null !== $customer->getEmail()) {
            try {
                $this->mailer->sendEmail(
                    $customer->getEmail(),
                    'Echeance '.$line->getPosition().'/'.$eflex->getNbLines().' payee - '.$eflex->getReference(),
                    $this->twig->render('emails/eflex_echeance_payee.html.twig', [
                        'customer' => $customer,
                        'eflex' => $eflex,
                        'line' => $line,
                    ]),
                    null,
                    null,
                    false,
                );
            } catch (\Throwable) {
                // silencieux
            }
        }

        // Notification admin
        try {
            $this->mailer->sendEmail(
                self::NOTIFICATION_EMAIL,
                'E-Flex '.$eflex->getReference().' - Echeance '.$line->getPosition().' payee - '.$customer->getFullName(),
                $this->twig->render('emails/eflex_echeance_payee.html.twig', [
                    'customer' => $customer,
                    'eflex' => $eflex,
                    'line' => $line,
                ]),
                null,
                null,
                false,
            );
        } catch (\Throwable) {
            // silencieux
        }

        return new JsonResponse(['status' => 'ok', 'action' => 'eflex_paid', 'position' => $line->getPosition()]);
    }

    /**
     * @codeCoverageIgnore
     */
    private function handleEFlexPaymentFailed(object $paymentIntent, int $eflexId, int $lineId, string $channel): JsonResponse
    {
        $line = $this->em->getRepository(\App\Entity\EFlexLine::class)->find($lineId);
        if (null === $line || $line->getEflex()->getId() !== $eflexId) {
            return new JsonResponse(['status' => 'ok', 'action' => 'eflex_line_not_found']);
        }

        $eflex = $line->getEflex();
        $errorMessage = $paymentIntent->last_payment_error->message ?? 'Echec prelevement';

        $line->setState(\App\Entity\EFlexLine::STATE_KO);
        $line->setFailureReason($errorMessage);
        $line->setStripePaymentIntentId($paymentIntent->id);
        $this->em->flush();

        $cancelled = false;
        if ($eflex->getNbFailed() >= 2) {
            $eflex->setState(\App\Entity\EFlex::STATE_CANCELLED);
            $this->em->flush();
            $cancelled = true;
        }

        $customer = $eflex->getCustomer();

        // Si annule : envoyer mail d'annulation au client + admin
        if ($cancelled) {
            if (null !== $customer->getEmail()) {
                try {
                    $this->mailer->sendEmail(
                        $customer->getEmail(),
                        'E-Flex '.$eflex->getReference().' annule - Rejets de prelevement',
                        $this->twig->render('emails/eflex_cancelled.html.twig', [
                            'customer' => $customer,
                            'eflex' => $eflex,
                        ]),
                        null,
                        null,
                        false,
                    );
                } catch (\Throwable) {
                    // silencieux
                }
            }

            try {
                $this->mailer->sendEmail(
                    self::NOTIFICATION_EMAIL,
                    'E-Flex '.$eflex->getReference().' annule (2 rejets) - '.$customer->getFullName(),
                    $this->twig->render('emails/eflex_cancelled.html.twig', [
                        'customer' => $customer,
                        'eflex' => $eflex,
                    ]),
                    null,
                    null,
                    false,
                );
            } catch (\Throwable) {
                // silencieux
            }
        }

        if (null !== $customer->getEmail()) {
            try {
                $payUrl = $this->urlGenerator->generate('app_eflex_pay', [
                    'id' => $eflex->getId(),
                    'lineId' => $line->getId(),
                ], UrlGeneratorInterface::ABSOLUTE_URL);

                $this->mailer->sendEmail(
                    $customer->getEmail(),
                    'Echec prelevement echeance '.$line->getPosition().'/'.$eflex->getNbLines().' - '.$eflex->getReference(),
                    $this->twig->render('emails/eflex_echeance_echec.html.twig', [
                        'customer' => $customer,
                        'eflex' => $eflex,
                        'line' => $line,
                        'errorMessage' => $errorMessage,
                        'payUrl' => $payUrl,
                    ]),
                    null,
                    null,
                    false,
                );
            } catch (\Throwable) {
                // silencieux
            }
        }

        // Notification admin
        try {
            $this->mailer->sendEmail(
                self::NOTIFICATION_EMAIL,
                'E-Flex '.$eflex->getReference().' - Echec echeance '.$line->getPosition().' - '.$customer->getFullName(),
                $this->twig->render('emails/eflex_echeance_echec.html.twig', [
                    'customer' => $customer,
                    'eflex' => $eflex,
                    'line' => $line,
                    'errorMessage' => $errorMessage,
                ]),
                null,
                null,
                false,
            );
        } catch (\Throwable) {
            // silencieux
        }

        return new JsonResponse(['status' => 'ok', 'action' => 'eflex_failed', 'position' => $line->getPosition()]);
    }

    /**
     * Cree un AdvertPayment pour une ligne d'echeancier payee, si un avis est lie.
     *
     * @codeCoverageIgnore
     */
    private function createAdvertPaymentForLine(Echeancier $echeancier, EcheancierLine $line): void
    {
        $advert = $echeancier->getAdvert();
        if (null === $advert) {
            return;
        }

        $payment = new AdvertPayment($advert, AdvertPayment::TYPE_SUCCESS, $line->getAmount());
        $payment->setMethod('sepa_debit');
        $payment->setEcheancier($echeancier);
        $this->em->persist($payment);
        $this->em->flush();
    }

    /**
     * Passe l'avis en accepted si un avis est lie a l'echeancier complete.
     *
     * @codeCoverageIgnore
     */
    private function markAdvertAsAccepted(Echeancier $echeancier): void
    {
        $advert = $echeancier->getAdvert();
        if (null === $advert) {
            return;
        }

        $advert->setState(Advert::STATE_ACCEPTED);
        $this->em->flush();
    }

    /**
     * Insere les paiements deja recus (lignes OK) dans l'avis lie.
     * Evite les doublons en verifiant si un AdvertPayment existe deja pour cette echeancier+ligne.
     *
     * @codeCoverageIgnore
     */
    private function syncPaidLinesToAdvert(Echeancier $echeancier): void
    {
        $advert = $echeancier->getAdvert();
        if (null === $advert) {
            return;
        }

        // Verifier les paiements deja lies a cet echeancier pour eviter les doublons
        $existingPayments = $this->em->getRepository(AdvertPayment::class)->findBy([
            'advert' => $advert,
            'echeancier' => $echeancier,
        ]);
        $existingCount = \count($existingPayments);

        $paidLines = 0;
        foreach ($echeancier->getLines() as $line) {
            if (EcheancierLine::STATE_OK === $line->getState()) {
                ++$paidLines;
                // Ne creer que les paiements manquants
                if ($paidLines > $existingCount) {
                    $payment = new AdvertPayment($advert, AdvertPayment::TYPE_SUCCESS, $line->getAmount());
                    $payment->setMethod('sepa_debit');
                    $payment->setEcheancier($echeancier);
                    $this->em->persist($payment);
                }
            }
        }

        $this->em->flush();
    }

    /**
     * Envoie les notifications de fin d'echeancier (attestation) au client et a l'admin.
     *
     * @codeCoverageIgnore
     */
    private function sendEcheancierCompletedNotifications(Echeancier $echeancier): void
    {
        $ref = $echeancier->getReference();

        // Generer le PDF attestation et l'envoyer a DocuSeal pour auto-signature
        // Le webhook DocuSeal (doc_type=echeancier_attestation) enverra le mail avec le PDF signe
        try {
            $pdf = new \App\Service\Pdf\EcheancierAttestationPdf($this->kernel, $echeancier);
            $pdf->generate();

            $tmpPath = tempnam(sys_get_temp_dir(), 'ech_att_').'.pdf';
            $pdf->Output('F', $tmpPath);

            $pdfBase64 = base64_encode(file_get_contents($tmpPath));

            $this->docuSealService->getApi()->createSubmissionFromPdf([
                'name' => 'Attestation '.$ref,
                'send_email' => false,
                'flatten' => true,
                'documents' => [[
                    'name' => 'attestation-'.$ref.'.pdf',
                    'file' => 'data:application/pdf;base64,'.$pdfBase64,
                ]],
                'submitters' => [[
                    'email' => 'client@e-cosplay.fr',
                    'name' => 'Association E-Cosplay',
                    'role' => 'First Party',
                    'completed' => true,
                    'send_email' => false,
                    'values' => ['Sign' => $this->docuSealService->getLogoBase64()],
                    'metadata' => [
                        'doc_type' => 'echeancier_attestation',
                        'echeancier_id' => $echeancier->getId(),
                    ],
                ]],
            ]);

            @unlink($tmpPath);

            $this->logger->info('Echeancier completed: attestation '.$ref.' envoyee a DocuSeal pour auto-signature');
        } catch (\Throwable $e) {
            $this->logger->error('Echeancier completed: erreur envoi attestation DocuSeal: '.$e->getMessage());
        }
    }

    /**
     * Envoie les notifications d'annulation apres 2 rejets au client et a l'admin.
     *
     * @codeCoverageIgnore
     */
    private function sendEcheancierCancelledAfterRejectsNotifications(Echeancier $echeancier): void
    {
        $customer = $echeancier->getCustomer();
        $ref = $echeancier->getReference();

        if (null !== $customer->getEmail()) {
            try {
                $this->mailer->sendEmail(
                    $customer->getEmail(),
                    'Echeancier '.$ref.' annule - Rejets de prelevement',
                    $this->twig->render('emails/echeancier_cancelled_rejects_client.html.twig', [
                        'customer' => $customer,
                        'echeancier' => $echeancier,
                    ]),
                    null,
                    null,
                    false,
                );
            } catch (\Throwable $e) {
                $this->logger->error('Echeancier cancelled rejects: erreur envoi mail client: '.$e->getMessage());
            }
        }

        try {
            $this->mailer->sendEmail(
                self::NOTIFICATION_EMAIL,
                'Echeancier '.$ref.' annule (2 rejets) - '.$customer->getFullName(),
                $this->twig->render('emails/echeancier_cancelled_rejects_admin.html.twig', [
                    'customer' => $customer,
                    'echeancier' => $echeancier,
                ]),
                null,
                null,
                false,
            );
        } catch (\Throwable $e) {
            $this->logger->error('Echeancier cancelled rejects: erreur envoi mail admin: '.$e->getMessage());
        }
    }

    /**
     * Genere le PDF de la facture, le sauvegarde via Vich, et l'envoie au client par mail.
     *
     * @codeCoverageIgnore
     */
    private function generateAndSendFacture(Facture $facture): void
    {
        $customer = $facture->getCustomer();

        // 1. Generer le PDF via FPDF
        $pdf = new \App\Service\Pdf\FacturePdf($this->kernel, $facture, $this->urlGenerator, $this->twig);
        $pdf->generate();

        $tmpPath = tempnam(sys_get_temp_dir(), 'facture_').'.pdf';
        $pdf->Output('F', $tmpPath);

        $uploadedFile = new UploadedFile(
            $tmpPath,
            'facture-'.str_replace('/', '-', $facture->getInvoiceNumber()).'.pdf',
            'application/pdf',
            null,
            true
        );

        $facture->setFacturePdfFile($uploadedFile);
        $facture->setUpdatedAt(new \DateTimeImmutable());
        $facture->setState(Facture::STATE_SEND);
        $this->em->flush();

        @unlink($tmpPath);

        $this->logger->info('Stripe webhook: PDF facture '.$facture->getInvoiceNumber().' genere');

        // 2. Envoyer au client
        if (null === $customer || null === $customer->getEmail()) {
            return;
        }

        $projectDir = $this->kernel->getProjectDir();
        $attachments = [];
        if (null !== $facture->getFacturePdf()) {
            $pdfPath = $projectDir.'/public/uploads/factures/'.$facture->getFacturePdf();
            if (file_exists($pdfPath)) {
                $attachments[] = ['path' => $pdfPath, 'name' => 'facture-'.str_replace('/', '-', $facture->getInvoiceNumber()).'.pdf'];
            }
        }

        $verifyUrl = $this->urlGenerator->generate('app_facture_verify', [
            'id' => $facture->getId(),
            'hmac' => $facture->getHmac(),
        ], UrlGeneratorInterface::ABSOLUTE_URL);

        $this->mailer->sendEmail(
            $customer->getEmail(),
            'Facture '.$facture->getInvoiceNumber(),
            $this->twig->render('emails/facture_send.html.twig', [
                'customer' => $customer,
                'facture' => $facture,
                'verifyUrl' => $verifyUrl,
            ]),
            null,
            null,
            false,
            $attachments,
        );

        $this->logger->info('Stripe webhook: facture '.$facture->getInvoiceNumber().' envoyee a '.$customer->getEmail());
    }
}
