<?php

namespace App\Utils;

use App\Entity\AccountAdherent;
use App\Entity\AccountComptoir;
use App\Entity\AccountGroupe;
use App\Entity\AccountPrestataire;
use App\Entity\AccountSiege;
use App\Entity\AchatMonnaie;
use App\Entity\Adherent;
use App\Entity\Comptoir;
use App\Entity\Flux;
use App\Entity\Groupe;
use App\Entity\OperationAdherent;
use App\Entity\OperationComptoir;
use App\Entity\OperationGroupe;
use App\Entity\OperationPrestataire;
use App\Entity\OperationSiege;
use App\Entity\Prestataire;
use App\Entity\Reconversion;
use App\Entity\Siege;
use App\Entity\Transaction;
use App\Entity\VenteEmlc;
use App\Enum\CurrencyEnum;
use App\Enum\MoyenEnum;
use App\Events\FluxEvent;
use App\Events\MLCEvents;
use App\Flux\AccountInterface;
use App\Flux\FluxInterface;
use Doctrine\DBAL\LockMode;
use Doctrine\ORM\EntityRepository;
use FOS\UserBundle\Model\UserInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\Security\Core\Security;

class OperationUtils
{
    private $em;
    private $session;
    private $security;
    private $eventDispatcher;

    public function __construct(CustomEntityManager $em, SessionInterface $session, Security $security, EventDispatcherInterface $eventDispatcher)
    {
        $this->em = $em;
        $this->session = $session;
        $this->security = $security;
        $this->eventDispatcher = $eventDispatcher;
    }

    private function getQueryOperations(EntityRepository $repo, AccountInterface $account, Request $request)
    {
        $qb = $repo
            ->createQueryBuilder('a')
            ->addSelect('flux')
            ->leftJoin('a.flux', 'flux')
            ->andWhere('a.account = :account')
            ->setParameter('account', $account->getId())
            ->orderBy('a.createdAt', 'DESC')
        ;

        if (!empty($request->get('formListOperations')['datemin'])) {
            $qb
                ->andWhere('a.createdAt >= :datemin')
                ->setParameter('datemin', $request->get('formListOperations')['datemin'])  // Date only comparison: error in queries comparing datetimes with some MYSQL versions
            ;
        }
        if (!empty($request->get('formListOperations')['datemax'])) {
            $qb
                ->andWhere('a.createdAt <= :datemax')
                ->setParameter('datemax', $request->get('formListOperations')['datemax'])
            ;
        }
        if (!empty($request->get('formListOperations')['moyen'])) {
            if (!in_array(($request->get('formListOperations')['moyen']), MoyenEnum::getAvailableTypes())) {
                throw new \InvalidArgumentException('Moyen de paiement invalide !');
            }
            $qb
                ->andWhere('flux.moyen = :moyen')
                ->setParameter('moyen', $request->get('formListOperations')['moyen'])
            ;
        }
        if (!empty($request->get('formListOperations')['showMonAPurchaseOnly'])) {
            $qb
                ->andWhere('flux.type IN (:type_achat_monnaie_adherent, :type_vente_emlc_adherent)')
                ->setParameter('type_achat_monnaie_adherent', AchatMonnaie::TYPE_ACHAT_ADHERENT)
                ->setParameter('type_vente_emlc_adherent', VenteEmlc::TYPE_VENTE_EMLC_ADHERENT)
            ;
        }

        return $qb->getQuery();
    }

    public function executeOperations(FluxInterface $flux)
    {
        try {
            $em = $this->em;
            $callback = function () use ($em, $flux) {
                $operations = $flux->getAllOperations($em);
                // 0/ Merge operations on same account (for cotisation+don and achatmonnaie+don) to check if balance is enough
                $operationsToCheck = [];
                foreach ($operations as $operation) {
                    if (!empty($operationsToCheck) && isset($operationsToCheck[$operation->getAccount()->getId()->toString()])) {
                        $op = $operationsToCheck[$operation->getAccount()->getId()->toString()];
                        $op->setMontant($op->getMontant() + $operation->getMontant());
                        $operationsToCheck[$operation->getAccount()->getId()->toString()] = $op;
                    } else {
                        $operationsToCheck[$operation->getAccount()->getId()->toString()] = $operation;
                    }
                }
                // 1/ Check if all operations can be done without erros
                foreach ($operationsToCheck as $operation) {
                    if (null == $operation->getAccount()) {
                        throw new \Exception('No account !!');
                    }
                    if ($operation->getAccount()->getCurrency() != $operation->getCurrency()) {
                        throw new \Exception("Account's currency different from operation !");
                    }

                    // Get repository inside callable to make sure EntityManager is valid
                    $class = $em->getClassMetadata(get_class($operation->getAccount()));
                    $accounts = $em->getRepository($class->getName());
                    // Fetch account with FOR UPDATE write lock
                    $account = $accounts->find(
                        $operation->getAccount()->getId(),
                        LockMode::PESSIMISTIC_WRITE
                    );

                    $newAccountBalance = $account->getBalance() + $operation->getMontant();
                    if ($newAccountBalance < 0) {
                        throw new \Exception('Operation impossible, montant supérieur au solde !');
                    }
                }
                // 2/ If all is ok, persist operations and new account's balance !
                foreach ($operations as $operation) {
                    // Get repository inside callable to make sure EntityManager is valid
                    $class = $em->getClassMetadata(get_class($operation->getAccount()));
                    $accounts = $em->getRepository($class->getName());
                    // Fetch account with FOR UPDATE write lock
                    $account = $accounts->find(
                        $operation->getAccount()->getId(),
                        LockMode::PESSIMISTIC_WRITE
                    );
                    // $account->addOperation($operation);
                    /*
                     * Apply operations on account amounts.
                     * One flux between two acounts is composed of two operations.
                     * These operations are configured by getAllOperations methods.
                     * These operations can be positive or negative.
                     * For example, a flux of type Reconversion will decrease
                     * accounts of prestataire and siege (see Reconversion.php).
                     */
                    $account->addAmount($operation->getMontant());
                    //Update special account "balance ccas" for some operations only
                    $updateBalanceCcas =
                        //only process isCcas or reconversionCcas $flux
                        ( $flux instanceof Transaction && $flux->getIsCcas() || $flux instanceof Reconversion && $flux->getReconversionCcas() )
                        //only process the prestataire side of the flux
                        && $account instanceof AccountPrestataire;
                    if ($updateBalanceCcas) {
                        $account->addAmountCcas($operation->getMontant());
                    }
                    $em->persist($operation);
                    $em->persist($account);
                }
                $em->persist($flux);
                $em->flush();
            };
            $this->em->transactional($callback);

            $this->eventDispatcher->dispatch(
                MLCEvents::FLUX,
                new FluxEvent($flux)
            );

            return true;
        } catch (\Exception $e) {
            // @TODO : tmp
            throw $e;
        }
    }

    public function getCurrentAccountable(UserInterface $user)
    {
        if (null != $this->session->get('_prestagere') && ($user->isGranted('ROLE_PRESTATAIRE') || $user->isGranted('ROLE_CAISSIER'))) {
            return $this->session->get('_prestagere');
        } elseif (null != $user->getAdherent() && $user->isGranted('ROLE_ADHERENT')) {
            return $user->getAdherent();
        } elseif (null != $this->session->get('_comptoirgere') && $user->isGranted('ROLE_COMPTOIR')) {
            return $this->session->get('_comptoirgere');
        } elseif (null != $this->session->get('_groupegere') && ($user->isGranted('ROLE_GESTION_GROUPE') || $user->isGranted('ROLE_CONTACT'))) {
            return $this->session->get('_groupegere');
        } elseif ($user->isGranted('ROLE_ADMIN_SIEGE')) {
            $siege = $this->em->getRepository(Siege::class)->getTheOne();

            return $siege;
        }

        return null;
    }

    /**
     * @return Account[] Returns an array of Account objects
     */
    public function getUserOperationsByCurrency(Request $request, string $currency = null)
    {
        if ($this->em->getFilters()->isEnabled('enabled_filter')) {
            $this->em->getFilters()->disable('enabled_filter');
        }
        if (null == $this->security->getUser()) {
            return null;
        }
        $object = $this->getCurrentAccountable($this->security->getUser());
        $repo = null;
        $account = null;
        if ($object instanceof Prestataire) {
            if (null != $currency) {
                $account = $this->em->getRepository(AccountPrestataire::class)->findOneBy(['prestataire' => $object, 'currency' => $currency]);
            } else {
                $account = $this->em->getRepository(AccountPrestataire::class)->findOneBy(['prestataire' => $object, 'currency' => CurrencyEnum::CURRENCY_EMLC]);
            }
            $repo = $this->em->getRepository(OperationPrestataire::class);
        } elseif ($object instanceof Adherent) {
            if (null != $currency) {
                $account = $this->em->getRepository(AccountAdherent::class)->findOneBy(['adherent' => $object, 'currency' => $currency]);
            } else {
                $account = $this->em->getRepository(AccountAdherent::class)->findOneBy(['adherent' => $object, 'currency' => CurrencyEnum::CURRENCY_EMLC]);
            }
            $repo = $this->em->getRepository(OperationAdherent::class);
        } elseif ($object instanceof Groupe) {
            $account = $this->em->getRepository(AccountGroupe::class)->findOneBy(['groupe' => $object, 'currency' => CurrencyEnum::CURRENCY_MLC]);
            $repo = $this->em->getRepository(OperationGroupe::class);
        } elseif ($object instanceof Comptoir) {
            $account = $this->em->getRepository(AccountComptoir::class)->findOneBy(['comptoir' => $object, 'currency' => CurrencyEnum::CURRENCY_MLC]);
            $repo = $this->em->getRepository(OperationComptoir::class);
        } elseif ($object instanceof Siege) {
            if (null != $currency) {
                if (!in_array($currency, CurrencyEnum::getAvailableTypes())) {
                    throw new \Exception('Opération impossible ! Type de currency  ' . $currency . ' inexistant');
                }
                $account = $object->getAccountWithCurrency($currency);
            } else {
                $account = $this->em->getRepository(AccountSiege::class)->findOneBy(['siege' => $object, 'currency' => CurrencyEnum::CURRENCY_MLC]);
            }
            $repo = $this->em->getRepository(OperationSiege::class);
        }
        if (null == $repo || null == $account) {
            return null;
        }

        return $this->getQueryOperations($repo, $account, $request);
    }
}
