<?php

declare(strict_types=1);

namespace App\Command;

use App\Entity\Adherent;
use App\Entity\Flux;
use App\Entity\GlobalParameter;
use App\Entity\Prestataire;
use App\Entity\SolidoumeItem;
use App\Entity\SolidoumeParameter;
use App\Entity\TransactionAdherentPrestataire;
use App\Entity\TransactionPrestataireAdherent;
use App\Entity\User;
use App\Enum\CurrencyEnum;
use App\Enum\MoyenEnum;
use App\Utils\CustomEntityManager;
use App\Utils\OperationUtils;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Twig\Environment;

/**
 * @SuppressWarnings(PHPMD.CyclomaticComplexity)
 */
class SolidoumeCommand extends Command
{
    protected static $defaultName = 'kohinos:solidoume:execute';

    protected $em;
    protected $logger;
    protected $mailer;
    protected $templating;
    protected $io;
    protected $param;
    protected $operationUtils;
    protected $isTest;
    protected $testExecute;
    protected $itemsTest;

    public function __construct(
        CustomEntityManager $em,
        LoggerInterface $cronLogger,
        \Swift_Mailer $mailer,
        Environment $templating,
        OperationUtils $operationUtils
    ) {
        $this->em = $em;
        $this->logger = $cronLogger;
        $this->mailer = $mailer;
        $this->templating = $templating;
        $this->operationUtils = $operationUtils;
        $this->isTest = false;
        $this->testExecute = false;
        $this->itemsTest = null;
        parent::__construct();
    }

    protected function configure()
    {
        $this
            ->setDescription('Sécurité sociale alimentaire : executer les opérations')
            ->setDefinition([
                new InputOption('test', null, InputOption::VALUE_NONE, 'For testing purpose : only print testresult of reminder email and taking money the right day'),
                new InputOption('execute', null, InputOption::VALUE_NONE, 'For testing purpose : execute program even if it\s not the correct date fo execution !'),
            ])
        ;
    }

    /**
     * Execute program Solidoume.
     *
     * @param InputInterface  $input
     * @param OutputInterface $output
     *
     * @return int
     */
    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $this->io = new SymfonyStyle($input, $output);
        $this->param = $this->em->getRepository(SolidoumeParameter::class)->findTheOne();
        if (empty($this->param)) {
            $this->io->error('Sécurité sociale alimentaire non paramétrée !');

            return 0;
        }
        $this->em->getConnection()->getConfiguration()->setSQLLogger(null);
        $this->isTest = $input->getOption('test');
        $this->testExecute = $input->getOption('execute');

        $this->io->title('Start');
        $this->executeReminders();
        // Envoi d'un quizz uniquement pour les utilisateurs de Solidoume !
        if ($this->em->getRepository(SolidoumeParameter::class)->getValueOf('name') == 'Solidoume') {
            $this->executeQuizz();
        }
        $this->executeTaking();
        sleep(1);
        $this->executeProgram();
        $this->io->success('End');

        $memoryUsage = memory_get_usage(true) / 1024 / 1024;
        $this->io->text("Batch finished with memory: ${memoryUsage}M");

        return 0;
    }

    /**
     * Execute quizz : send quizz to all participant who have participated since 3 months minimum
     */
    private function executeQuizz()
    {
        $this->io->title('START : Envoi du questionnaire');
        $items = $this->em->getRepository(SolidoumeItem::class)->findBy(['enabled' => true]);
        foreach ($items as $item) {
            if ($this->hasToSendQuizz($item)) {
                $item->setQuizzSended(true);
                if (!$this->isTest) {
                    $this->em->persist($item);
                    $this->em->flush();
                }
                $this->sendQuizz($item);
            }
        }
    }

    /**
     * Execute email reminder : If balance of account if not enough x days before date of paiement => send email.
     */
    private function executeReminders()
    {
        $this->io->title('START : Rappel par email');
        $items = $this->em->getRepository(SolidoumeItem::class)->findBy(['enabled' => true]);
        
        foreach ($items as $item) {
            if ($this->hasToExecuteReminders($item)) {
                $amount = $item->getAmount();
                $accountEmlc = $item->getAdherent()->getAccountWithCurrency(CurrencyEnum::CURRENCY_EMLC);
                if (null != $accountEmlc) {
                    if ($accountEmlc->getBalance() < $amount) {
                        // L'adherent n'a pas assez d'argent sur son ecompte !
                        if ($this->isTest) {
                            $this->io->error('Balance : ' . $accountEmlc->getBalance() . ', montant : ' . $amount);
                        }
                        $this->sendReminder($item);
                    }
                } else {
                    $this->io->error("L'adherent " . $adherent->__toString() . " n'a pas de compte emlc !");
                }
            }
        }
    }

    /**
     * Send email reminder.
     *
     * @param SolidoumeItem $solidoumeItem
     */
    private function sendReminder(SolidoumeItem $solidoumeItem)
    {
        $adherent = $solidoumeItem->getAdherent();
        $this->io->success("Envoi de l'email de rappel à l'adhérent : " . $adherent->__toString());

        $subject = $this->em->getRepository(SolidoumeParameter::class)->getValueOf('name') . ' : Votre compte n\'a pas un solde suffisant !';
        $mail = (new \Swift_Message($subject))
            ->setFrom($this->em->getRepository(GlobalParameter::class)->val(GlobalParameter::MLC_NOTIF_EMAIL))
            ->setTo($adherent->getUser()->getEmail())
            ->setBody(
                $this->templating->render(
                    '@kohinos/email/solidoume/reminder.html.twig',
                    [
                        'subject' => $subject,
                        'item' => $solidoumeItem,
                    ]
                ),
                'text/html'
            );
        if (!$this->isTest) {
            $this->mailer->send($mail);
        }
    }

    /**
     * Send Quizz email.
     *
     * @param SolidoumeItem $solidoumeItem
     */
    private function sendQuizz(SolidoumeItem $solidoumeItem)
    {
        $adherent = $solidoumeItem->getAdherent();
        $this->io->success("Envoi du questionnaire à l'adhérent : " . $adherent->__toString());

        $subject = 'Vos retours sur ' . $this->em->getRepository(SolidoumeParameter::class)->getValueOf('name') . ' !';
        $mail = (new \Swift_Message($subject))
            ->setFrom($this->em->getRepository(GlobalParameter::class)->val(GlobalParameter::MLC_NOTIF_EMAIL))
            ->setTo($adherent->getUser()->getEmail())
            ->setBody(
                $this->templating->render(
                    '@kohinos/email/solidoume/quizz.html.twig',
                    [
                        'subject' => $subject,
                        'item' => $solidoumeItem,
                    ]
                ),
                'text/html'
            );
        if (!$this->isTest) {
            $this->mailer->send($mail);
        }
    }

    /**
     * Take money of participant of solidoume if date of today is same as paiementDate.
     */
    private function executeTaking()
    {
        $this->io->title('START : prélèvement du jour');
        $nowDay = (new \DateTime('now', new \DateTimeZone('UTC')))->format('d');
        $nowMonth = intval((new \DateTime('now', new \DateTimeZone('UTC')))->format('Ym'));
        $items = $this->em->getRepository(SolidoumeItem::class)->findBy(['enabled' => true, 'paiementDate' => $nowDay]);

        try {
            $em = $this->em;
            $isTest = $this->isTest;
            $io = $this->io;
            $operationUtils = $this->operationUtils;
            $callback = function () use ($em, $items, $isTest, $io, $nowDay, $nowMonth, $operationUtils) {
                foreach ($items as $item) {
                    // Prélèvement des emlc sur chaque compte adhérent participant au programme
                    $amount = $item->getAmount();
                    $accountEmlc = $item->getAdherent()->getAccountWithCurrency(CurrencyEnum::CURRENCY_EMLC);
                    if (null != $accountEmlc
                        && $item->getPaiementDate() == $nowDay
                        && $item->getLastMonthPayed() != $nowMonth) {
                        if ($accountEmlc->getBalance() < $amount) {
                            $io->error($item->getAdherent()->__toString() . ' : Solde de l\'adherent inférieur au montant à prélever (Solde : ' . $accountEmlc->getBalance() . ' eMLC) !');
                            $item->setEnabled(false);
                            if (!$isTest) {
                                $em->persist($item);
                            }
                            // @TODO : alerter l'utilisateur ?
                        } else {
                            $flux = new TransactionAdherentPrestataire();
                            $flux->setExpediteur($item->getAdherent());
                            $flux->setDestinataire($em->getRepository(Prestataire::class)->getPrestataireSolidoume());
                            $flux->setMontant($amount);
                            $flux->setData($item->toArray());
                            $flux->setMoyen(MoyenEnum::MOYEN_EMLC);
                            $now = (new \Datetime('now'))->format('d/m/Y H:i:s');
                            $flux->setReference($em->getRepository(SolidoumeParameter::class)->getValueOf('name') . ' prélèvement en date du ' . $now);

                            $item->setLastMonthPayed($nowMonth);

                            if (!$isTest) {
                                $em->persist($flux);
                                $em->persist($item);
                                // Write operations for this flux !
                                $operationUtils->executeOperations($flux);
                                $em->flush();
                            } else {
                            }

                            $io->success("Prélèvement $amount € \nFlux : " . $flux->__toString() . "\nItem : " . $item->__toString());
                        }
                    }
                }
            };
            $this->em->transactional($callback);
        } catch (\Exception $e) {
            // @TODO : tmp
            throw $e;
        }
    }

    /**
     * Execute solidoume repartition of money recolted if date of today is same as executionDate.
     */
    private function executeProgram()
    {
        $nowDay = (new \DateTime('now', new \DateTimeZone('UTC')))->format('d');

        if (!$this->testExecute && $nowDay != $this->param->getExecutionDate()) {
            $this->io->warning("Ce n'est pas le jour d'execution du programme (" . $this->param->getExecutionDate() . ')');
        } else {
            $this->io->title('START : Répartition de la somme récoltée');
            $items = $this->em->getRepository(SolidoumeItem::class)->findBy(['enabled' => true]);
            
            $total = 0;
            $countPerson = 0;
            $countParticipants = 0;
            $participants = [];
            foreach ($items as $item) {
                if ($this->isItemPayedThisMonth($item)) {
                    $datas = $this->em->getRepository(Flux::class)->getQueryByAdherentAndDestinataire($item->getAdherent(), $this->em->getRepository(Prestataire::class)->getPrestataireSolidoume(), 'adherent_prestataire');
                    $lastPrelevement = null;
                    foreach ($datas as $data) {
                        if ($data->getCreatedAt()->format('d') <= $this->param->getExecutionDate() && $data->getCreatedAt()->format('m') == ((new \DateTime('now', new \DateTimeZone('UTC')))->format('m'))) {
                            $lastPrelevement = $data;
                        } elseif ($data->getCreatedAt()->format('d') > $this->param->getExecutionDate() && $data->getCreatedAt()->format('m') == ((new \DateTime('now', new \DateTimeZone('UTC')))->format('m')-1)) {
                            $lastPrelevement = $data;
                        }
                    }
                    if ($lastPrelevement != null) {
                        $datas = $this->em->getRepository(Flux::class)->getQueryByAdherentAndDestinataire($item->getAdherent(), $this->em->getRepository(Prestataire::class)->getPrestataireSolidoume(), 'prestataire_adherent');
                        $lastPaiement = null;
                        foreach ($datas as $data) {
                            if ($data->getCreatedAt()->format('m') == ((new \DateTime('now', new \DateTimeZone('UTC')))->format('m'))) {
                                $lastPaiement = $data;
                            }
                        }
                        if ($lastPaiement == null) {
                            $total += $lastPrelevement->getMontant();
                            if (!$item->getIsDonation()) {
                                ++$countParticipants;
                            }
                            $participants[] = $item;
                            ++$countPerson;
                        }
                    }
                }
            }
            $totalByParticipant = round((($total / $countParticipants) * ((100 - $this->param->getCommission()) / 100)), 2, PHP_ROUND_HALF_DOWN);
            $this->io->success('Total de eMLC récolté : ' . $total . ' !');
            $this->io->success('Nombre de personnes : ' . $countPerson . ' !');
            $this->io->success('Nombre de participants au programme : ' . $countParticipants . ' !');
            $this->io->success('Total par participants (sans commission) : ' . ($total / $countParticipants) . ' !');
            $this->io->success('Total par participants (avec commission de ' . $this->param->getCommission() . '%) : ' . $totalByParticipant . ' !');
            try {
                $em = $this->em;
                $isTest = $this->isTest;
                $io = $this->io;
                $operationUtils = $this->operationUtils;
                $callback = function () use ($em, $participants, $isTest, $io, $totalByParticipant, $operationUtils) {
                    foreach ($participants as $item) {
                        if ($this->isItemPayedThisMonth($item) && !$item->getIsDonation()) {
                            // Envoi de l'emlc sur le compte de l'adherent
                            $flux = new TransactionPrestataireAdherent();
                            $flux->setExpediteur($em->getRepository(Prestataire::class)->getPrestataireSolidoume());
                            $flux->setDestinataire($item->getAdherent());
                            $flux->setMontant($totalByParticipant);
                            $flux->setMoyen(MoyenEnum::MOYEN_EMLC);
                            $now = (new \Datetime('now'))->format('d/m/Y H:i:s');
                            $flux->setReference('Correction ' . $em->getRepository(SolidoumeParameter::class)->getValueOf('name') . ' ' . $now);
                            if (!$isTest) {
                                $em->persist($flux);
                                // Write operations for this flux !
                                $operationUtils->executeOperations($flux);
                                $em->flush();
                            }
                            $io->success("Paiement solidoume\nFlux : " . $flux->__toString() . "\nItem : " . $item->__toString());
                        }
                        if (!$item->getIsRecurrent()) {
                            // Disabled all non recurrent paiement
                            $item->setEnabled(false);
                            if (!$isTest) {
                                $em->persist($item);
                            } else {
                                $io->text('Paiement non recurrent donc désactivé ! ' . $item->__toString());
                            }
                        }
                    }
                    if (!$isTest) {
                        $em->flush();
                        $em->clear();
                    }
                };
                $this->em->transactional($callback);
            } catch (\Exception $e) {
                $this->io->error($e->getMessage());
            }
        }

        return null;
    }

    private function isItemPayedThisMonth(SolidoumeItem $item): bool
    {
        $nowMonth = intval((new \DateTime('now', new \DateTimeZone('UTC')))->format('Ym'));
        $lastMonth = intval((new \DateTime('now -1month', new \DateTimeZone('UTC')))->format('Ym'));
        if ($item->isEnabled() && null != $item->getLastMonthPayed()) {
            if ($item->getPaiementDate() <= $this->param->getExecutionDate() && $item->getPaiementDate() >= 1) {
                if ($item->getLastMonthPayed() >= $nowMonth) {
                    return true;
                }
            } else {
                if ($item->getLastMonthPayed() >= $lastMonth) {
                    return true;
                }
            }
        }

        return false;
    }

    private function hasToSendQuizz(SolidoumeItem $item)
    {
        $datas = $this->em->getRepository(Flux::class)->getQueryByAdherentAndDestinataire($item->getAdherent(), $this->em->getRepository(Prestataire::class)->getPrestataireSolidoume(), 'adherent_prestataire');

        if (count($datas) >= 3 && !$item->isQuizzSended()) {
            return true;
        }

        return false;
    }

    /**
     * Has to execute reminder by email ? Check date of paiement choose by adherent and number of days before remind.
     *
     * @param SolidoumeItem $item
     *
     * @return bool
     */
    private function hasToExecuteReminders(SolidoumeItem $item)
    {
        $nowDay = intval((new \DateTime('now', new \DateTimeZone('UTC')))->format('d'));
        $reminderDays = $this->param->getReminderDays();
        $executionDate = intval($item->getPaiementDate());
        if ($nowDay > $executionDate) {
            $d = new \DateTime('now');
            $d->modify('first day of next month');
            $nextExecution = $d->format('Ym') . ($executionDate < 10 ? '0' . $executionDate : $executionDate);
            $reminingDate = (new \DateTime('+' . $reminderDays . ' days', new \DateTimeZone('UTC')))->format('Ymd');
            if ($reminingDate == $nextExecution) {
                return true;
            }
        } else {
            $nextExecution = (new \DateTime('now', new \DateTimeZone('UTC')))->format('Ym') . ($executionDate < 10 ? '0' . $executionDate : $executionDate);
            $reminingDate = (new \DateTime('+' . $reminderDays . ' days', new \DateTimeZone('UTC')))->format('Ymd');
            if ($reminingDate == $nextExecution) {
                return true;
            }
        }

        return false;
    }
}