<?php

namespace App\Controller;

use App\Entity\GlobalParameter;
use App\Entity\Payment;
use App\Entity\User;
use App\Repository\PaymentRepository;
use App\Security\LoginAuthenticator;
use App\Utils\PaymentUtils;
use Doctrine\ORM\EntityManagerInterface;
use Payum\Core\Payum;
use Payum\Core\Request\GetHumanStatus;
use Payum\Core\Request\Notify;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\Form;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Guard\GuardAuthenticatorHandler;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
use Symfony\Component\Translation\TranslatorInterface;

/**
 * Gestion des paiements avec Payum.
 */
class PaymentController extends AbstractController
{
    protected $em;
    protected $translator;
    protected $payum;
    protected $authenticator;
    protected $guardHandler;
    protected $paymentUtils;

    public function __construct(
        EntityManagerInterface $em,
        TranslatorInterface $translator,
        LoginAuthenticator $authenticator,
        GuardAuthenticatorHandler $guardHandler,
        Payum $payum,
        PaymentUtils $paymentUtils
    ) {
        $this->em = $em;
        $this->translator = $translator;
        $this->payum = $payum;
        $this->authenticator = $authenticator;
        $this->guardHandler = $guardHandler;
        $this->paymentUtils = $paymentUtils;
    }

    /**
     * Crée une instance de Payment, les tokens associés, et redirige vers la page de paiement.
     */
    public function preparePaymentAction(Form $form, $type, $extra_data = null)
    {
        /* @var PaymentRepository $repo */
        $repo = $this->em->getRepository(Payment::class);
        //Redirect to starting payment page if a valid starting payment page exists
        $url = $repo->findUrlOfValidStartingPayment($this->getUser()->getUsername());
        $this->em->flush(); //save status updates when looking to valid starting payment page
        if ($url) {
            return $this->redirect($url);
        }

        // Enregistre les données du Flux en json, pour l'enregistrer une fois le paiement validé
        $serializer = $this->container->get('serializer');
        $toSerialize = Payment::TYPE_ADHESION == $type ? $form->get('cotisation')->getData() : $form->getData();
        $data = $serializer->normalize(
            $toSerialize,
            null,
            [AbstractNormalizer::ATTRIBUTES => [
                'reference',
                'moyen',
                'montant',
                'role',
                'don' => [
                    'reference',
                    'moyen',
                    'montant',
                    'role',
                    'type',
                    'expediteur' => ['id'],
                    'destinataire' => ['id'],
                    'operateur' => ['id'],
                ],
                'expediteur' => ['id'],
                'destinataire' => ['id'],
                'operateur' => ['id'], ],
            ]
        );

        $jsondata = $serializer->serialize($data, 'json');

        // Prepare CB Payment
        if ('true' === $this->em->getRepository(GlobalParameter::class)->val(GlobalParameter::USE_PAYZEN)) {
            $gatewayName = 'payzen';
        } else {
            $this->addFlash(
                'error',
                $this->translator->trans('Une erreur est survenue due à la configuration du paiement dans l\'application. Il est pour l\'instant impossible de payer par CB, merci de contacter votre monnaie locale.')
            );

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

        $storage = $this->payum->getStorage('App\Entity\Payment');

        $payment = $storage->create();
        $payment->setNumber(uniqid());
        $payment->setCurrencyCode('978');
        $payment->setDescription($type);
        $payment->setFluxData($jsondata);

        // Data to persist when payment is valid (other than Flux data)
        if (null != $extra_data) {
            $payment->setExtraData($extra_data);
        }

        if (Payment::TYPE_ADHESION == $type) {
            $payment->setTotalAmount($form->get('cotisation')->get('montant')->getData() * 100); // 1.23 EUR
            $payment->setClientId('Nouvel adhérent');
            $payment->setClientEmail($form->get('user')->get('email')->getData());
        } else {
            if ($form->has('don') && $form->get('don')->getData()->getMontant() > 0) {
                $payment->setTotalAmount(($form->get('montant')->getData() * 100) + ($form->get('don')->getData()->getMontant() * 100)); // 1.23 EUR
            } else {
                $payment->setTotalAmount($form->get('montant')->getData() * 100); // 1.23 EUR
            }

            $payment->setClientId($this->getUser()->getId());
            $payment->setClientEmail($this->getUser()->getEmail());
        }

        if (Payment::TYPE_PAIEMENT_RECURRENT_COTISATION_TAV == $type) {
            $payment->setRecurrenceAmount($form->get('montant')->getData() * 100);
            $payment->setIsRecurrent(true);
            $payment->setRecurrenceMonthsCount($form->get('nombreMois')->getData());
            $payment->setRecurrenceMonthDay($form->get('jourPrelevement')->getData());
        }

        $storage->update($payment);

        $captureToken = $this->payum->getTokenFactory()->createCaptureToken(
            $gatewayName,
            $payment,
            'payment_done' // the route to redirect after capture
        );

        // Symfony creates URLs with http and not https -> replace
        $targetUrl = preg_replace('/^http:/', 'https:', $captureToken->getTargetUrl());
        $afterUrl = preg_replace('/^http:/', 'https:', $captureToken->getAfterUrl());

        $captureToken->setTargetUrl($targetUrl);
        $captureToken->setAfterUrl($afterUrl);

        $targetUrl = $captureToken->getTargetUrl();
        /* @var Payment $payment */
        if (!$payment->getPayzenStartingPaymentUrl()) {
            $payment->setPayzenStartingPaymentUrl($targetUrl);
        }

        $this->em->persist($captureToken);
        $this->em->flush();

        return $this->redirect($targetUrl);
    }

    /* COMMENT LES FLUX SONT-ILS CREES SUITE A LA CREATION D'UN PAIEMENT PAYZEN ?
     *
     *
     *
     * CAS 1 : Paiement standard
     * Deux voies de signalement coexistent :
     *
     * Voie 1 : via la notification instantannée payzen.
     * Un paramètre appelé "URL de notification à la fin du paiement" est configuré dans le backoffice payzen et
     * est renseigné à payement/notify. Cette valeur est utilisée par payzen, sauf si la variable vads_url_check
     * est transmise, auquel cas c'est cette dernière valeur qui prime.
     * Ici, vads_url_check est transmise à la valeur payment/notify/{token}.
     * Remarque : son usage systématique n'est pas recommandé par Payzen.
     * L'URL est ainsi captée par NotifyController.php.
     *
     * Voie 2 : via la redirection du client.
     * Cette voie n'est pas garantie (si l'utilisateur ferme son navigateur assez tôt, il ne sera pas redirigé).
     * A vérifier : l'URL de redirection est la combinaison entre une configuration côté backoffice et
     * un complément que nous transmettons.
     * Lorsque la redirection a bien lieu, c'est donc le contrôleur doneAction de PaymentController.php qui est sollicité,
     * avec une requête au format payement/done/?payum_token={token}
     *
     * Remarquons dans ces deux stratégie la présence d'un token permettant l'authentification de l'agent Payzen et
     * la récupération de l'objet paiement préalablement enregistré en base.
     *
     *
     *
     * CAS 2 : Paiement récurrent
     *
     * Dans le cadre de notre implémentation, un paiement récurrent commence par un paiement initial (occurence n°0).
     * Pour cette occurence n°0, au niveau du mode de communication, on est sur un processus en apparence similaire au
     * CAS 1. Un examen minutieux de ce process m'a cependant permis de mettre en évidence des comportements que
     * je n'ai pas réussi à comprendre :
     * En ouvrant les vannes pour tester le déclenchement d'une création de flux à chaque notification rattachée
     * à un paiement récurrent, j'ai observé la génération de pas moins de 5 événements, opérant donc
     * un accroissement de solde de +750 mona au lieu de +150 mona !!!
     * - 2 événements via /payement/notify/{token}
     * - 3 événements après l'execution de la ligne $gateway->execute($status = new GetHumanStatus($token)); dans doneAction.
     *
     * Le fonctionnement du module Payum est sans doute très intelligent mais il est trop complexe à maitriser pour moi.
     * Il fait également appel à des mécanismes dont l'usage systématique est déconseillé par Payzen (utilisation de la variable
     * vads_url_check).
     *
     * Bref. De toute façon, la gestion des notifications successives d'un paiement récurrent ne peut pas être gérée
     * via les voies 1 ou 2 en l'état.
     * En effet la variable vads_url_check ne fonctionne pas en mode récurrent et Payzen nous notifie à l'aide
     * d'une requête post au format de l'"URL de notification à la création d'un abonnement" (mal nommé), configurée à payement/notify,
     * (mais on aurait pu mettre autre chose pour plus de clarté). Il faut donc créer un controlleur spécifique qui va traiter
     * cette notification sans disposer du token utilisé par les voies 1 ou 2.
     *
     * Voie 3 : le traitement de la requête se fait finalement simplement dans notifyRecurringPaymentAction sans utiliser
     * les mécanismes complexes de Payum.
     *
     * Pour d'autres problèmes avec Payum, voir aussi les commentaires liés à
     * l'utilisation de GetHumanStatus::STATUS_AUTHORIZED.
     *
     * Yvon KERDONCUFF
     * 26/03/2024
     */

    /**
     * Collects payzen recurring payment payment occurence in place of default payum system.
     *
     * Payum default controler is not able to catch URL recurring payment notifications
     * because payzen actual used URL has shape /payment/notify instead of /payment/notify/token.
     *
     * @param Request $request
     *
     * @return Response
     * @Route("/payment/notify", name="notify_recurring_payment")
     */
    public function notifyRecurringPaymentAction(Request $request)
    {
        $vads_cust_email = $request->request->get('vads_cust_email');
        //Look for recurring payments from this client.
        $recurringPayments = $this->em->getRepository(Payment::class)->findBy([
            'isRecurrent' => true,
            'clientEmail' => $vads_cust_email,
        ]);

        if (!$recurringPayments) {
            //return error
            return new Response('No recurring payments with email ' . $vads_cust_email, 405);
        }

        $vads_identifier = $request->request->get('vads_identifier');
        $vads_trans_status = $request->request->get('vads_trans_status');
        $new_status = strtolower($vads_trans_status);

        foreach ($recurringPayments as $payment) {
            //Just look for one valid payment.
            if (
                $payment->getDetails()
                && array_key_exists('vads_identifier', $payment->getDetails())
                && $payment->getDetails()['vads_identifier'] == $vads_identifier
            ) {
                // Payum GetHumanStatus constants don't match Payzen vads_trans_status here
                // e.g. GetHumanStatus::STATUS_AUTHORIZED does not match 'authorised' value sent by Payzen
                if (in_array($new_status, ['captured', 'authorised', GetHumanStatus::STATUS_CAPTURED, GetHumanStatus::STATUS_AUTHORIZED])) {
                    $this->paymentUtils->handlePayzenNotificationCore($payment);
                    $this->em->flush();
                }
                //for debug purpose, display vads_trans_status in payzen backoffice
                return new Response('Recurring payment occurence taken into account. vads_trans_status = ' . $new_status, 200);
            }
        }
        //return error
        return new Response('No recurring payments with vads_identifier ' . $vads_identifier, 405);
    }

    /**
     * Ce contrôleur est sollicité lorsque Payzen renvoie le cotisant sur l'URL de retour,
     *
     * @Route("/payment/done/", name="payment_done")
     */
    public function doneAction(Request $request)
    {
        try {
            $token = $this->payum->getHttpRequestVerifier()->verify($request);
        } catch (\Exception  $e) {
            // Token expired
            return $this->redirectToRoute('index');
        }

        // Get payment
        $gateway = $this->payum->getGateway($token->getGatewayName());
        $gateway->execute($status = new GetHumanStatus($token));
        $payment = $status->getFirstModel();

        if (GetHumanStatus::STATUS_NEW == $payment->getStatus()) {
            $gateway->execute(new Notify($token));
        } else {
            $this->payum->getHttpRequestVerifier()->invalidate($token);
        }

        // Set flash message according to payment status
        /* WARNING : THIS PIECE OF CODE IS USING INAPPROPRIATE GetHumanStatus::STATUS_AUTHORIZED :
         *
         * This is (another) issue here with Payum.
         * current_payment_status will never match STATUS_AUTHORIZED
         * on the opposite, we have no chance to detect 'authorised' value here
         * because Payzen vads_trans_status is written 'authorised' and not 'authorized'
         *
         * @see comment above notifyRecurringPaymentAction for more Payum weird stuff
        */
        if (in_array($payment->getStatus(), ['captured', 'authorised', GetHumanStatus::STATUS_CAPTURED, GetHumanStatus::STATUS_AUTHORIZED])) {
            $type = $payment->getDescription();

            if (Payment::TYPE_ACHAT_MONNAIE_ADHERENT == $type || Payment::TYPE_ACHAT_MONNAIE_PRESTA == $type) {
                $this->addFlash(
                    'success',
                    $this->translator->trans('Achat de monnaie locale bien effectué !')
                );
            } elseif (Payment::TYPE_COTISATION_ADHERENT == $type || Payment::TYPE_COTISATION_PRESTA == $type) {
                $this->addFlash(
                    'success',
                    $this->translator->trans('Cotisation bien reçue. Merci !')
                );
            } elseif (Payment::TYPE_ADHESION == $type) {
                $this->addFlash(
                    'success',
                    $this->translator->trans('Votre adhésion a bien été prise en compte, bienvenue !')
                );

                // Connect new user
                return $this->guardHandler
                ->authenticateUserAndHandleSuccess(
                    $this->em->getRepository(User::class)->findOneBy(['id' => $payment->getClientId()]),
                    $request,
                    $this->authenticator,
                    'main'
                );
            } elseif (Payment::TYPE_PAIEMENT_COTISATION_TAV == $type || Payment::TYPE_PAIEMENT_RECURRENT_COTISATION_TAV) {
                $this->addFlash(
                    'success',
                    $this->translator->trans('Cotisation payée !')
                );
            }
        } elseif (
            GetHumanStatus::STATUS_CANCELED == $payment->getStatus() ||
                    GetHumanStatus::STATUS_EXPIRED == $payment->getStatus() ||
                    GetHumanStatus::STATUS_FAILED == $payment->getStatus()
        ) {
            $this->addFlash(
                'error',
                $this->translator->trans('La transaction a été annulée.')
            );
        }

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