<?php

namespace App\Utils;

use App\Entity\Adherent;
use App\Entity\CotisationTavPrelevementCorrectionSolde;
use App\Entity\CotisationTavPrelevementDepassementPlafond;
use App\Entity\CotisationTavReversementCorrectionSolde;
use App\Entity\GlobalParameter;
use App\Entity\Payment;
use App\Entity\Siege;
use App\Entity\Flux;
use App\Entity\CotisationTavReversement;
use App\Entity\CotisationTavPrelevement;
use App\Entity\ThirdPartyAllocationFunding;
use App\Entity\Allocation;
use App\Enum\MoyenEnum;
use App\Repository\PaymentRepository;
use App\Utils\CustomEntityManager;
use Payum\Core\Request\GetHumanStatus;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\DependencyInjection\ContainerInterface;

class TAVCotisationUtils
{
    private $em;
    private $security;
    private $operationUtils;
    private $container;

    public function __construct (
        CustomEntityManager $em,
        Security $security,
        OperationUtils $operationUtils,
        ContainerInterface $container
    ) {
        $this->em = $em;
        $this->security = $security;
        $this->operationUtils = $operationUtils;
        $this->container = $container;
    }

    /**
     * Check if there is an active recurring paiement or if cotisation already exist this month
     * by returning a descriptive reason string if so, else an empty string.
     */
    public function preventCotisationDuplication(Adherent $adherent)
    {
        $email = $adherent->getUser()->getEmail();
        //Look for existing recurring payment
        if($reason = $this->checkExistingRecurringPayment($email)) {
            return implode(" ", array_column($reason,'reason'));
        }
        //Look for existing cotisation
        if ($this->checkExistingCotisation($adherent)) {
            return "Cotisation déjà payée ce mois-ci.";
        }
        //Look for possible Payzen starting payment (neither finished nor expired yet)
        /* @var PaymentRepository $repo */
        $repo = $this->em->getRepository(Payment::class);
        $foundStartingPaymentTimeout = $repo->findValidStartingPayment($email);
        if ($foundStartingPaymentTimeout) {
            return "Détection d'un possible paiement déjà en cours. Le paiement sera de nouveau possible à "
                . $foundStartingPaymentTimeout->format("H:i:s") . ", horaire d'expiration du paiement en cours.";
        }
        return "";
    }

    /**
     * Check if cotisation already exist this month.
     */
    private function checkExistingCotisation(Adherent $adherent)
    {
        $first_day_this_month = date('Y-m-01');
        $last_day_this_month  = date('Y-m-t');

        $existing = $this->em->getRepository(Flux::class)->getTavCotisationsBetweenDates(
            $adherent,
            $first_day_this_month,
            $last_day_this_month
        );

        return count($existing) > 0;
    }

    /**
     * Returns an array containing a descriptive string of existing payment
     * as well as other information about the payment, or an empty array.
     * 
     * @param String $userEmail
     */
    public function checkExistingRecurringPayment($userEmail)
    {
        $recurringPayments = $this->em->getRepository(Payment::class)->findBy([
            'isRecurrent' => true,
            'clientEmail' => $userEmail,
        ]);

        $res = [];
        foreach($recurringPayments as $p) {
            if (
                $p->getStatus() !== GetHumanStatus::STATUS_FAILED
                && $p->getStatus() !== GetHumanStatus::STATUS_CANCELED
                && $p->getStatus() !== GetHumanStatus::STATUS_EXPIRED
                && $p->getDetails()
                && array_key_exists('vads_identifier',$p->getDetails()) //some payment without vads_identifier have status NEW, which are not valid payments
            ) {
                //Everytime payzen sends a recurring payment notification, notification is
                //caught by notifyRecurringPaymentAction, which does not update payment status.
                //This is why we can not rely on $p->getStatus to decide if a recurring
                //payment is still active or ended or expired.
                $reason = "";
                $monthNumberSinceInitialPaymentDate = -1;
                if($p->isRecurringPaymentEndedOrExpired($reason,$monthNumberSinceInitialPaymentDate) !== true) {
                    $res[] = ['reason' => $reason, 'monthNumberSinceInitialPaymentDate' => $monthNumberSinceInitialPaymentDate];
                }
            }
        }
        return $res;
    }

    /**
     * First method to calculate allowance: 
     * according to a contribution rate defined in user's profile (ProfilDeCotisation).
     * 
     * Apply the cotisation profile rate to the amount paid 
     * and register the complement as a new flux (only if rate != 1)
     * 
     * Warning: EntityManager not flushed here.
     */
    public function applyTauxCotisation(Flux $flux)
    {
        $adherent = $flux->getDestinataire();
        $profile = $adherent->getProfilDeCotisation();
        $cotisationTaux = $profile->getTauxCotisation();

        // create the Allocation
        $allocation = new Allocation();
        $allocation->setAdherent($adherent);
        $allocation->setCotisationFlux($flux);
        
        // don't need to create an other Flux if the rate is 1
        if ($cotisationTaux != 1) {
            // calculate the mlc amount the user will receive
            $cotisationAmount = $profile->getMontant();
            $mlcAmount = round($cotisationAmount * $cotisationTaux);
            $allocation->setAmount($mlcAmount);
    
            // get the difference between what the user paid and what he•she's supposed to receive
            $amountDiff = $mlcAmount - $cotisationAmount;
    
            if ($flux->getExpediteur() instanceof Siege) {
                $siege = $flux->getExpediteur();
            } else {
                $siege = $flux->getExpediteur()->getGroupe()->getSiege();
            }
    
            if ($amountDiff > 0) {
                // User should receive more than he•she paid: send a new flux to the user to complete their cotisation
                $fluxCotis = new CotisationTavReversement();
                $fluxCotis->setExpediteur($siege);
                $fluxCotis->setDestinataire($adherent);
                $fluxCotis->setMontant($amountDiff);
                $fluxCotis->setReference("Versement de l'allocation complémentaire après paiement de " . $cotisationAmount . "€ pour atteindre une allocation de " . $mlcAmount . " MonA");
            
                // Create third party funding instance if required
                if ($adherent->getThirdPartyFinancer() !== null) { 
                    // $this->createThirdPartyAllocationFunding($adherent, $fluxCotis);
                    $this->createThirdPartyAllocationFunding($adherent, $flux, $fluxCotis, 100, $cotisationAmount, $amountDiff);

                }
            } else {
                // User should receive less than he•she paid: fetch the difference from his account 
                $fluxCotis = new CotisationTavPrelevement();
                $fluxCotis->setExpediteur($adherent);
                $fluxCotis->setDestinataire($siege);
                $fluxCotis->setMontant(-$amountDiff);
                $fluxCotis->setReference("Prélèvement sur la cotisation après paiement de " . $cotisationAmount . "€ pour atteindre une allocation de " . $mlcAmount . " MonA");
            }
    
            $fluxCotis->setOperateur($flux->getOperateur());
            $fluxCotis->setRole($flux->getRole());
            $fluxCotis->setMoyen(MoyenEnum::MOYEN_EMLC);
            $this->em->persist($fluxCotis);

            $allocation->setComplementaryFlux($fluxCotis);
            $this->em->persist($allocation);

            $this->operationUtils->executeOperations($fluxCotis);
        } else {
            $cotisationAmount = $profile->getMontant();
            $allocation->setAmount($cotisationAmount);
            $this->em->persist($allocation);
            $this->em->flush();
        }
    }

    /**
     * Calculate & return allowance value in household mode.
     * 
     * Rules are as follow:
     * - [SSA_HOUSEHOLD_BASE_AMOUNT] emlc for the first person in user's household
     * - [SSA_HOUSEHOLD_SECONDARY_AMOUNT] emlc for each other adult
     * - [SSA_HOUSEHOLD_SECONDARY_AMOUNT] emlc for each dependant child above [SSA_HOUSEHOLD_DEPENDANT_CHILD_LIMIT_AGE]
     * - if SSA_HOUSEHOLD_DEPENDANT_CHILD_UNDER_LIMIT_AMOUNT is defined:
     *      [SSA_HOUSEHOLD_DEPENDANT_CHILD_UNDER_LIMIT_AMOUNT] emlc for each dependant child under [SSA_HOUSEHOLD_DEPENDANT_CHILD_LIMIT_AGE]
     *   else 
     *      [SSA_HOUSEHOLD_SECONDARY_AMOUNT] for all dependant child
     * - if SSA_HOUSEHOLD_USE_SHARED_CUSTODY is true:
     *   apply a percentage if the child is in shared custody: 25%, 50% or 75% depending on the shared custody arrangement
     * 
     * @param Adherent $adherent
     */
    public function getCalculatedHouseholdAllowanceValue($adherent) {
        $adultsCount = $adherent->getHouseholdAdultCount();
        if ($adultsCount == null) {
            return null;
        }

        // base allowance, for the first adult
        $mlcAllowanceAmount = (int) $this->em->getRepository(GlobalParameter::class)
            ->val(GlobalParameter::SSA_HOUSEHOLD_BASE_AMOUNT);

        // Get amount for the other houshold members
        $mclSecondaryAmount = (int) $this->em->getRepository(GlobalParameter::class)
            ->val(GlobalParameter::SSA_HOUSEHOLD_SECONDARY_AMOUNT);

        // increment for each other adult in the household
        $mlcAllowanceAmount += $mclSecondaryAmount * ($adultsCount - 1);

        // increment allowance for each dependant child, depending on the shared custody arrangement
        $useSharedCustody = $this->em->getRepository(GlobalParameter::class)
            ->val(GlobalParameter::SSA_HOUSEHOLD_USE_SHARED_CUSTODY);
        $childUnderLimitAmount = (int) $this->em->getRepository(GlobalParameter::class)
            ->val(GlobalParameter::SSA_HOUSEHOLD_DEPENDANT_CHILD_UNDER_LIMIT_AMOUNT);

        $dependentChildren = $adherent->getDependentChildren();
        foreach ($dependentChildren as $child) {
            $childAllowanceAmount = $mclSecondaryAmount;

            // Different value for children under limit age, if activated
            if (false === $child->getOlderThanLimitAge() && null !== $childUnderLimitAmount) {
                $childAllowanceAmount = (int) $childUnderLimitAmount;
            }

            // Apply shared custody percentage if activated and set
            if ('true' === $useSharedCustody) {
                $sharedCustodyPercentage = $child->getSharedCustodyPercentage();
                if ($sharedCustodyPercentage != null) {
                   $childAllowanceAmount = $childAllowanceAmount * $sharedCustodyPercentage;
                }
            }

            $mlcAllowanceAmount += $childAllowanceAmount;
        }

        return $mlcAllowanceAmount;
    }

    /**
     * Second method to calculate allowance: 
     * allowance based on user's household.
     * 
     * See @getCalculatedHouseholdAllowanceValue() for calculations implentation without verifications
     * 
     * @param Adherent $adherent (by ref)
     */
    public function calculateAllowanceAccordingToHousehold(&$adherent) {
        // If forced amount activated, set value
        $forcedAmount = (int) $this->em->getRepository(GlobalParameter::class)
            ->val(GlobalParameter::SSA_FORCE_ALLOCATION_AMOUNT);
        if($forcedAmount > 0) {
            $adherent->setAllocationAmount($forcedAmount);
            return;
        }        

        // Calculation
        $mlcAllowanceAmount = $this->getCalculatedHouseholdAllowanceValue($adherent);

        // Null returned in case of missing necessary param: exit
        if ($mlcAllowanceAmount === null) {
            return;
        }

        // Apply cap if activated in configuration
        $maxAllocationAmount = $this->em->getRepository(GlobalParameter::class)
            ->val(GlobalParameter::SSA_HOUSEHOLD_MAX_ALLOCATION_AMOUNT);

        if (null !== $maxAllocationAmount && 0 !== intval($maxAllocationAmount) && $mlcAllowanceAmount > intval($maxAllocationAmount)) {
            $mlcAllowanceAmount = intval($maxAllocationAmount);
        }

        $adherent->setAllocationAmount($mlcAllowanceAmount);
    }

    /**
     * Third method to calculate allowance. Based on household as well, but in a simplified manner.
     * 
     * Rules as follow:
     * 1 person: 100 emlc
     * 2 person: 150 emlc
     * 3 person: 180 emlc
     * 4+ person: 220 emlc
     */
    public function calculateAllowanceAccordingToHouseholdSimplified(&$adherent) {

        $forcedAmount = (int) $this->em->getRepository(GlobalParameter::class)
            ->val(GlobalParameter::SSA_FORCE_ALLOCATION_AMOUNT);
        if($forcedAmount > 0) {
            $adherent->setAllocationAmount($forcedAmount);
            return;
        }

        $householdCount = $adherent->getHouseholdCount();

        if (is_null($householdCount) || $householdCount == 0) {
            return;
        }

        if ($householdCount == 1) {
            $mlcAllowanceAmount = 100;
        } else if ($householdCount == 2) {
            $mlcAllowanceAmount = 150;
        }  else if ($householdCount == 3) {
            $mlcAllowanceAmount = 180;
        }  else {
            $mlcAllowanceAmount = 220;
        } 

        $adherent->setAllocationAmount($mlcAllowanceAmount);
    }

    /**
     * Method called to create Flux based on allowance amount (for household based allowance).
     * Only create flux if amount paid != allowance amount.
     * Also creates an Allocation instance.
     * Also creates a ThirdPartyAllocationFunding instance if needed.
     */
    public function applyHouseholdAllowance(Flux $flux) {
        // get allowance
        $adherent = $flux->getDestinataire();
        $cotisationAmount = $flux->getMontant();
        
        // get the mlc amount the user is supposed to receive
        $mlcAllowanceAmount = $adherent->getAllocationAmount();

        // if allocation amount is null (as a security but should not occur), calculate here before applying & creating flux
        if (is_null($mlcAllowanceAmount)) {
            if ($this->container->getParameter('simplified_household_based_allowance')) {
                $this->calculateAllowanceAccordingToHouseholdSimplified($adherent);
            } else {
                $this->calculateAllowanceAccordingToHousehold($adherent);
            }
            $this->em->persist($adherent);
            $mlcAllowanceAmount = $adherent->getAllocationAmount();
        }

        // create the Allocation
        $allocation = new Allocation();
        $allocation->setAdherent($adherent);
        $allocation->setCotisationFlux($flux);
        $allocation->setAmount($mlcAllowanceAmount);

        // get the difference between what the user paid and what he•she's supposed to receive
        $amountDiff = $mlcAllowanceAmount - $cotisationAmount;

        // only create new flux if there is a difference
        if ($amountDiff != 0) {
            if ($flux->getExpediteur() instanceof Siege) {
                $siege = $flux->getExpediteur();
            } else {
                $siege = $flux->getExpediteur()->getGroupe()->getSiege();
            }

            if ($amountDiff > 0) {
                // User should receive more than he•she paid: send a new flux to the user to complete its cotisation
                $fluxCotis = new CotisationTavReversement();
                $fluxCotis->setExpediteur($siege);
                $fluxCotis->setDestinataire($adherent);
                $fluxCotis->setMontant($amountDiff);
                $fluxCotisRef = "Versement de l'allocation complémentaire après paiement de " . $cotisationAmount . "€ pour atteindre une allocation de " . $mlcAllowanceAmount . " MonA";
                $fluxCotis->setReference($fluxCotisRef);
            
                // Create third party funding instance if required
                if ($adherent->getThirdPartyFinancer() !== null) { 
                    $this->createThirdPartyAllocationFunding($adherent, $flux, $fluxCotis, $mlcAllowanceAmount, $cotisationAmount, $amountDiff);
                }
            } else {
                // User should receive less than he•she paid: fetch the difference from his account 
                $fluxCotis = new CotisationTavPrelevement();
                $fluxCotis->setExpediteur($adherent);
                $fluxCotis->setDestinataire($siege);
                $fluxCotis->setMontant(-$amountDiff);
                $fluxCotis->setReference("Réduction de l'allocation correspondant à un paiement de " . $cotisationAmount . "€ pour atteindre une allocation de " . $mlcAllowanceAmount . " MonA.");
            }
    
            $fluxCotis->setOperateur($flux->getOperateur());
            $fluxCotis->setRole($flux->getRole());
            $fluxCotis->setMoyen(MoyenEnum::MOYEN_EMLC);
            $this->em->persist($fluxCotis);

            $allocation->setComplementaryFlux($fluxCotis);
            $this->em->persist($allocation);

            $this->operationUtils->executeOperations($fluxCotis);
        } else {
            $this->em->persist($allocation);
            $this->em->flush();
        }
    }

    /**
     * Create the ThirdPartyAllocationFunding from adherent and cotisation flux data
     */
    private function createThirdPartyAllocationFunding(
        $adherent,
        $flux,
        &$fluxCotis,
        $mlcAllowanceAmount,
        $cotisationAmount,
        $amountDiff
    ) {
        $thirdparty = $adherent->getThirdPartyFinancer();
        $fundingAmount = $thirdparty->getFundingAmount();

        $thirdPartyAllocationFunding = new ThirdPartyAllocationFunding();
        $thirdPartyAllocationFunding->setThirdPartyFinancer($thirdparty);
        $thirdPartyAllocationFunding->setAmount($fundingAmount);
        $thirdPartyAllocationFunding->setAdherent($adherent);
        $thirdPartyAllocationFunding->setCotisationFlux($flux);
        $thirdPartyAllocationFunding->setAllocationFlux($fluxCotis);
        $thirdPartyAllocationFunding->setAllocationAmount($mlcAllowanceAmount);

        $ref = "Prise en charge du tiers financeur $thirdparty d'un montant de " . $fundingAmount . "€ pour l'adhérent " . $adherent . ", pour sa cotisation de " . $cotisationAmount . "€ avec reversement de $amountDiff MonA";
        $thirdPartyAllocationFunding->setReference($ref);
        $this->em->persist($thirdPartyAllocationFunding);

        // Update allocation flux reference to include third party
        $fluxCotisRef = $fluxCotis->getReference();
        $fluxCotisRef .= " ; dont prise en charge de " . $fundingAmount . "€ par le tiers financeur $thirdparty";
        $fluxCotis->setReference($fluxCotisRef);
    }

    /**
     * Method called to create Flux based on allowance amount (for household based allowance).
     */
    public function withdrawDownToTheCeiling(Adherent $adherent)
    {
        $balance = $adherent->getEmlcAccount()->getBalance();
        $ceiling = $adherent->getCeiling();
        $siege = $this->em->getRepository(Siege::class)->getTheOne();

        // get the amount we want to withdraw
        $amountDiff = $ceiling - $balance;

        if ($amountDiff >= 0) {
            throw new \Exception("Impossible de prélèver : le solde de l'adhérent est inférieur ou égal au plafond.");
        }

        $flux = new CotisationTavPrelevementDepassementPlafond();
        $flux->setExpediteur($adherent);
        $flux->setDestinataire($siege);
        $flux->setMontant(-$amountDiff);
        $flux->setReference("Prélèvement pour ramener le solde de " . $balance . " MonA sous le plafond de " . $ceiling . " MonA.");
        $flux->setOperateur($this->security->getUser());
        $flux->setRole($this->security->getUser()->getGroups()[0]->__toString());
        $flux->setMoyen(MoyenEnum::MOYEN_EMLC);
        $this->em->persist($flux);
        $this->operationUtils->executeOperations($flux);

        return $amountDiff;
    }

    /**
     * Method called to create Flux to fix balance (for household based allowance).
     */
    public function fixBalance(Adherent $adherent, $fixedBalance, $justification)
    {
        $balance = $adherent->getEmlcAccount()->getBalance();
        $siege = $this->em->getRepository(Siege::class)->getTheOne();

        $amountDiff = $fixedBalance - $balance;

        if ($amountDiff >= 0) {
            //Accroissement du solde
            $flux = new CotisationTavReversementCorrectionSolde();
            $flux->setExpediteur($siege);
            $flux->setDestinataire($adherent);
            $flux->setReference(
                "Reversement pour corriger le solde de " . $balance . " MonA à " . $fixedBalance . " MonA : " . $justification
            );
        } else {
            //Réduction du solde
            $flux = new CotisationTavPrelevementCorrectionSolde();
            $flux->setExpediteur($adherent);
            $flux->setDestinataire($siege);
            $flux->setReference(
                "Prélèvement pour corriger le solde de " . $balance . " MonA à " . $fixedBalance . " MonA : " . $justification
            );
        }
        $flux->setMontant(abs($amountDiff));
        $flux->setOperateur($this->security->getUser());
        $flux->setRole($this->security->getUser()->getGroups()[0]->__toString());
        $flux->setMoyen(MoyenEnum::MOYEN_EMLC);
        $this->em->persist($flux);
        $this->operationUtils->executeOperations($flux);
    }

    /**
     * Get the last cotisation of an adhérent
     *
     * @param Adherent $adherent
     *
     * @return bool|date
     */
    public function getLastTavCotisationForAdherent(?Adherent $adherent)
    {
        $cotisations = [];
        if (null !== $adherent) {
            $cotisations = $this->em->getRepository(Flux::class)->getLastTavCotisation($adherent);
        }

        if (count($cotisations) > 0) {
            return $cotisations[0]["created_at"];
        }

        return false;
    }

    /**
     * Mark active recurring payment(s) status as canceled.
     * 
     * @param String $userEmail
     */
    public function cancelExistingRecurringPayment($userEmail) {
        $recurringPayments = $this->em->getRepository(Payment::class)->findBy([
            'isRecurrent' => true,
            'clientEmail' => $userEmail,
        ]);

        foreach($recurringPayments as $p) {
            if (
                $p->getStatus() !== GetHumanStatus::STATUS_FAILED
                && $p->getStatus() !== GetHumanStatus::STATUS_CANCELED
                && $p->getStatus() !== GetHumanStatus::STATUS_EXPIRED
                && $p->getDetails()
                && array_key_exists('vads_identifier',$p->getDetails()) //some payment without vads_identifier have status NEW, which are not valid payments
            ) {
                $reason = "";
                $monthNumberSinceInitialPaymentDate = -1;
                if($p->isRecurringPaymentEndedOrExpired($reason,$monthNumberSinceInitialPaymentDate) !== true) {
                    $p->setStatus(GetHumanStatus::STATUS_CANCELED);
                    $this->em->persist($p);
                }
            }
        }
        $this->em->flush();
    }

    /**
     * Check parameters and Adherent data in order to detect the right allocation method to use.
     * 
     * Specific rule: if simplified_household_based_allowance is active BUT adherent's profile is incomplete for this method AND (s)he has a ProfilDeCotisation set, use ProfilDeCotisation
     * (allows flowless transition from ProfilDeCotisation to simplified_household_based_allowance)
     * 
     * @param Adherent $adherent
     * @return String 'cotisation_profile' | 'household_based_allowance' | 'simplified_household_based_allowance
     */
    public function getAppropriateAllocationMethod($adherent) {
        if ($this->container->getParameter('simplified_household_based_allowance')) {
            if (
                (is_null($adherent->getCotisationAmount()) || is_null($adherent->getHouseholdCount()))
                && !is_null($adherent->getProfilDeCotisation())
            ) {
                return 'cotisation_profile';
            } else {
                return 'simplified_household_based_allowance';
            }
        } else if ($this->container->getParameter('household_based_allowance')) {
            return 'household_based_allowance';
        } else {
            return 'cotisation_profile';
        }
    }
}
