<?php
namespace Modules\Commande\Controller;

use Container\Mail\CdeModifPlPr;
use Entity\Model\CaEntet;
use Entity\Model\CaEvent;
use Entity\Model\CaLigne;
use Entity\Model\CaPlCt;
use Entity\Model\CaPlPr;
use Entity\Model\Fourniss;
use Entity\Model\Hastutil;
use Entity\Model\Paravrac;
use Entity\Model\Unites;
use GuzzleHttp\Psr7\Response;
use Helpers\Ariane\Ariane;
use Helpers\Log\LogSysteme;
use Helpers\Outil\ArrayHelper;
use Helpers\Outil\Date;
use Helpers\Outil\Mail;
use Helpers\Outil\Roles;
use Helpers\Outil\Sql;
use Helpers\Trad\Trad;
use Helpers\Validator\Validator;
use Modules\Commande\Repository\LigneCommande;
use Modules\GlobalApplication\Repository\Incidents;
use Skeleton\Controller\Controller;
use Helpers\Parametres\Constante;

class PlanningController extends Controller
{

    public function formUpdateAction() : string
    {
        $commandeId = $this->request->getAttribute('commandeId');

        $commande = (new CaEntet($this->pdo))->find($commandeId);
        if (empty($commande)) {
            throw new \Exception('Invalid commande #'.$commandeId);
        }
        $trad = $this->getTranslationService();

        $fourniss = (new Fourniss($this->pdo))->find($commande['CODE_FOU']);

        $listCaLigne = (new LigneCommande($this->pdo))->getLigneCommande($commandeId);

        $planningContractuel = (new CaPlCt($this->pdo))->findAll(['NO_CA' => $commandeId]);
        $planningContractuel = ArrayHelper::indexBy($planningContractuel, 'NO_PO_CA');


        $planningPrevisionel = (new CaPlPr($this->pdo))->findAll(['NO_CA' => $commandeId]);
        $planningPrevisionel = ArrayHelper::indexBy($planningPrevisionel, 'NO_PO_CA');
        
        $listCaLigne = array_map(function ($item) use ($planningContractuel, $planningPrevisionel) {
                return $item + [
                    'planningContractuel' => $planningContractuel[ $item['NO_PO'] ] ?? [],
                    'planningPrevisionel' => $planningPrevisionel[ $item['NO_PO'] ] ?? [],
                ];
        }, $listCaLigne);

        $title = Trad::strBuild(
            $trad->t('Modification date de livraison de la commande @1@ du fournisseur @2@ (@3@)'),
            $commande['NO_CDE'],
            $fourniss['RS'],
            $fourniss['CODE']
        );
        Ariane::generate(1, $trad->t('Modification date de livraison'));
        ariane::setFocus($commandeId);

        return $this->renderer->render('@Commande/Planning/updatePlanning', [
            'title' => $title,
            'commande' => $commande,
            'listCaLigne' => $listCaLigne,
            'mapUnite' => array_column((new Unites($this->pdo))->fetchAll(), null, 'CODE_UNI'),
        ]);
    }


    public function updateAction() :Response
    {
        $post = $this->request->getParsedBody();
        $commandeId = $this->request->getAttribute('commandeId');
        $trad = $this->getTranslationService();
        
        $commande = (new CaEntet($this->pdo))->find($commandeId);
        
        if (empty($commande)) {
            throw new \Exception('Attempt to save AccRecep on invalid commande "'.$commandeId.'"');
        }

        if (! in_array($commande['ETAT_CDE'], ['E','L'])) {
            throw new \Exception('Attempt to save AccRecep on invalid commande "'.$commandeId.'"');
            // TODO : log & redirect instead ?
        }

        if (! Roles::droitsUtilisateur($this->pdo, 'HA_CA_DPR', $_SESSION['Session_Login'])) {
            throw new \Exception('User #'.$_SESSION['Session_Login'].' has insufficient rights');
        }

        
        // Load CaLigne.Demandeur
        $listCaLigne = (new LigneCommande($this->pdo))
            ->getForPlanning($commande['NO_CDE']);

        //___________________
        // Validation

        $validator = $this->createValidator();
        $planningPrevNew = [];
        foreach ($listCaLigne as $caLigne) {
            $planningPrevNew[ $caLigne['NO_PO'] ] = $post['planning'][ $caLigne['NO_PO'] ] ?? [];
            $ligneVali = $this->validatePlanning(
                $caLigne,
                $planningPrevNew[ $caLigne['NO_PO'] ]
            );

            if (! $ligneVali->isValid()) {
                $validator->setError(
                    '<br>' . Trad::strBuild(
                        $trad->t('Ligne @1@') . $trad->colon(),
                        $caLigne['NO_PO'],
                    )
                    . $ligneVali->getError()
                );
            }
        }

        /** @var array<string,array{DTE_LIV:string,QTE_COMM:string}[]> $planningPrevNew */

        // Validate incident
        $incident = $post['incident'] ?? null;
        if ($incident !== null) {
            if (! isset($incident['DESC'])
                || ! isset($incident['MEMO'])
            ) {
                LogSysteme::prefixErrorLog('This should never happen');
                return new Response(400, [], $this->renderer->render(
                    '@popup-erreur/ErreurCreation',
                    [ 'ErreurValid' => 'Invalid request']
                ));
            }

            $validator->isStringAvecEspace(
                $trad->t('Description'),
                $incident['DESC'],
                2,
                60
            );
            $validator->isStringAvecEspace(
                $trad->t('Commentaire'),
                $incident['MEMO'],
                0,
                3000
            );
        }

        // Case : has error -> skip further validation
        if (! $validator->isValid()) {
            return new Response(400, [], $this->renderer->render(
                '@popup-erreur/ErreurCreation',
                [ 'ErreurValid' => $validator->getError() ]
            ));
        }

        //___________________
        // Update DB

        // Get PlanningPrevisionnel before update
        // Required for the post-processes
        /** @var array<string,array<array{DTE_LIV:string,QTE_COMM:string}>> */
        $planningPrevOld = ArrayHelper::indexBy(
            (new CaPlPr($this->pdo))->findAll(['NO_CA' => $commande['NO_CDE']]),
            'NO_PO_CA'
        );

        // Get PlanningContractuel
        /** @var array<string,array<array{DTE_LIV:string,QTE_COMM:string}>> */
        $planingCtr = ArrayHelper::indexBy(
            (new CaPlCt($this->pdo))->findAll(['NO_CA' => $commande['NO_CDE']]),
            'NO_PO_CA'
        );

        if (! $this->pdo->beginTransaction()) {
            throw new \Exception('!!!');
        }

        // Update incident
        if ($incident != null) {
            // Create an CaEvent for each CaLigne with a changing planning
            foreach ($listCaLigne as $caLigne) {
                $caLigneId = $caLigne['NO_PO'];

                // Filter : planning for this CaLigne didn't change
                if ($this->isPlanningEqual(
                    ArrayHelper::filterByKey($planningPrevOld, [$caLigneId]),
                    ArrayHelper::filterByKey($planningPrevNew, [$caLigneId]),
                )) {
                    continue;
                }

                (new Incidents($this->pdo))
                    ->createIncidentReportLiv(
                        $caLigne['NO_CDE'],
                        $caLigne['NO_PO'],
                        $incident['DESC'],
                        $incident['MEMO']
                    );
            }
        }
        
        // Update planning
        $entPlanning = (new CaPlPr($this->pdo));

        // Reset
        $entPlanning->delete([
            'NO_CA' => $commande['NO_CDE'],
        ]);

        foreach ($planningPrevNew as $caLigneId => $listItem) {
            foreach ($listItem as $item) {
                // TODO : merge into one query
                $entPlanning->insert([
                    'NO_CA' => $commande['NO_CDE'],
                    'NO_PO_CA' => $caLigneId,
                    'DTE_LIV' => $item['DTE_LIV'],
                    'QTE_COMM' => $item['QTE_COMM'],
                    'CODE_STE' => $commande['CODE_STE'],
                ]);
            }
        }

        // Update CaLigne DTELIVPRE
        $qbCaLigne = (new CaLigne($this->pdo));
        foreach ($planningPrevNew as $caLigneId => $listItem) {
            // TODO : merge into one query
            $qbCaLigne->update(
                [
                    'DTELIVPRE' => count($listItem) == 1 ?
                        reset($listItem)['DTE_LIV'] : '00000000',
                ],
                [
                    'NO_CDE' => $commande['NO_CDE'],
                    'NO_PO' => $caLigneId,
                ]
            );
        }

        // Update CaEntet.CAEN_ECART_LIVR
        (new CaEntet($this->pdo))->update(
            ['CAEN_ECART_LIVR' => ! $this->isPlanningEqual($planingCtr, $planningPrevNew)],
            $commande['NO_CDE']
        );

        // Create event
        (new CaEvent($this->pdo))->insertEntity(
            $commande['NO_CDE'],
            $commande['AVENANT'],
            'PLA',
            $_SESSION['Session_Login'],
            '0'
        );

        if (!$this->pdo->commit()) {
            throw new \Exception('!!!');
        }

        //___________________
        // Post processes

        $isEmailEnabled = Sql::toBool(
            (new Paravrac($this->pdo))->getGeneric('EMA_AUT_GENERAL')
        );
        $isEmailDemEnabled = Sql::toBool(
            (new Paravrac($this->pdo))->getGeneric('EMAIL_DEM_MODIF_PLANNING')
        );

        if ($isEmailDemEnabled && $isEmailEnabled) {
            $commande['Acheteur'] = (new Hastutil($this->pdo))->find($commande['ACH']);
            $commande['Fourniss'] = (new Fourniss($this->pdo))->find($commande['CODE_FOU']);

            // Index by Demandeur
            $listCaLigneByDemandeur = [];
            foreach ($listCaLigne as $caLigne) {
                $caLigneId = $caLigne['NO_PO'];

                // Filter : planning for this CaLigne didn't change
                if ($this->isPlanningEqual(
                    ArrayHelper::filterByKey($planningPrevOld, [$caLigneId]),
                    ArrayHelper::filterByKey($planningPrevNew, [$caLigneId]),
                )) {
                    continue;
                }

                $demId = $caLigne['DaDemandeur']['CODE']
                    ?? $caLigne['CaDemandeur']['CODE'];
                $listCaLigneByDemandeur[ $demId ][] = $caLigne;
            }

            $mapUnit = array_column(
                (new Unites($this->pdo))->fetchAll(),
                null,
                'CODE_UNI'
            );

            // Send email to each Demandeur
            foreach ($listCaLigneByDemandeur as $listCaLigne) {
                // Get demandeur
                // assume every caLigne has been filtered to have the same Demandeur
                $demandeur = $listCaLigne[0]['DaDemandeur']
                    ?? $listCaLigne[0]['CaDemandeur'];
                $acheteur = $commande['Acheteur'];

                // Case : Demandeur has disabled this email
                if (! Sql::toBool($demandeur['EMA_DEM_MODIF_PL_CDE'])) {
                    continue;
                }

                $mailData = [
                    'NUM_CDE' => $commande['NO_CDE'],
                    'CODE_FOU' => $commande['Fourniss']['CODE'],
                    'RS_FOU' => $commande['Fourniss']['RS'],
                    'NOM_DEMANDEUR' => $demandeur['NOM'],
                    'NOM_ACHETEUR' => $acheteur['NOM'],
                    'DETAILS' => CdeModifPlPr::renderDetail(
                        $listCaLigne,
                        $planningPrevOld,
                        $planningPrevNew,
                        $mapUnit,
                        $trad
                    ),
                ];
                
                /** @see CdeModifPlPr */
                Mail::generateMail(
                    $this->pdo,
                    $trad,
                    $demandeur['EMAIL'],
                    'CDE_MODIF_PL_PR',
                    $demandeur['COD_LNG'],
                    $mailData
                );

                // Create event
                (new CaEvent($this->pdo))->insertEntity(
                    $commande['NO_CDE'],
                    $commande['AVENANT'],
                    'MPL',
                    $_SESSION['Session_Login'],
                    Sql::updateMemo(
                        $this->pdo,
                        '',
                        $trad->t('Mail à un demandeur') . $trad->colon()
                        . $demandeur['CODE'] . ' (' . $demandeur['NOM'] . ' - '.$demandeur['CODE'].')' . "\r\n"
                        . Trad::strBuild(
                            $trad->t('Modification du planning pour la commande : @1@'),
                            $mailData['NUM_CDE']
                        ) . "\r\n"
                        . "\r\n"
                        . Trad::strBuild(
                            $trad->t('Notification pour la modification du planning Prévisionnel de la commande @1@'),
                            $mailData['NUM_CDE']
                        ) . "\r\n"
                        . "\r\n"
                        . $trad->t('Détail') . $trad->colon() . "\r\n"
                        . "\r\n"
                        . $mailData['DETAILS']
                        . "\r\n"
                        . "\r\n"
                        . $trad->t('Demandeur') . $trad->colon() . $demandeur['NOM'] . ' - '. $demandeur['CODE'] . "\r\n"
                        . "\r\n"
                        . $trad->t('Acheteur') . $trad->colon() . $acheteur['NOM'] . ' - '. $acheteur['CODE'] ."\r\n"
                    )
                );
            }
        }

        // Success
        return new Response(200);
    }

//_____________________________________________________________________________
// Sub-routine

// TODO : move into a validator
    /**
     * @param array<mixed> $caLigne
     * @param array<mixed> $planning
     */
    private function validatePlanning(
        array $caLigne,
        array &$planning
    ) :Validator {
        $validator = $this->createValidator();
        $trad = $this->getTranslationService();

        if (empty($planning)) {
            $validator->setError($trad->t('Une ligne de planning est obligatoire'));
            return $validator;
        }

        foreach ($planning as &$item) {
            if (!isset($item['DTE_LIV']) || !isset($item['QTE_COMM'])) {
                throw new \Exception('!!');
            }
            $validator->isDate($item['DTE_LIV']);
            if ($validator->isFloat(
                'Quantité', // TODO : indicate line id
                $item['QTE_COMM'],
                Constante::formatQTE,
                true,
                null,
                0
            )) {
                if ($item['QTE_COMM'] == 0) {
                    $validator->setError($trad->t('Il ne peut y avoir qu\'une ligne de planning pour cette ligne de commande ayant une quantité à 0.'));
                }
            }
        }
        unset($item);

        // Check each date is unique
        $a = array_count_values(array_column($planning, 'DTE_LIV'));
        foreach ($a as $date => $count) {
            if ($count > 1) {
                $validator->setError(
                    Trad::strBuild(
                        $trad->t('La date @1@ est dupliquée sur le planning.'),
                        (new Date())->dateConverte($date)
                    )
                );
            }
        }

        // Case : not valid -> skip further validation
        if (! $validator->isValid()) {
            return $validator;
        }

        $total = array_sum(array_column($planning, 'QTE_COMM'));
        if ($total != $caLigne['QTE_COMM']) {
            $validator->setError(
                $trad->t('La somme des quantités du planning est différente de celle de la ligne.')
            );
            return $validator;
        }
        return $validator;
    }


    /**
     * @param array<string,array{DTE_LIV:string,QTE_COMM:string}[]> $planningCtr item planning (date/quantite) indexed by CaLigne Id
     * @param array<string,array{DTE_LIV:string,QTE_COMM:string}[]> $planningPrev item planning (date/quantite) indexed by CaLigne Id
     */
    private function isPlanningEqual(
        array $planningCtr,
        array $planningPrev
    ) :bool {


        foreach ($planningCtr as $caLigne => $listItemCt) {
            $listItemPrev = $planningPrev[ $caLigne ] ?? null;
            if ($listItemPrev === null) {
                return false;
            }
            if (count($listItemPrev) != count($listItemCt)) {
                return false;
            }

            $listItemPrev = array_values($listItemPrev);
            $listItemCt = array_values($listItemCt);
            usort($listItemPrev, [$this,'fnSortDate']);
            usort($listItemCt, [$this,'fnSortDate']);

            foreach ($listItemCt as $i => $itemCt) {
                $itemPrev = $listItemPrev[ $i ];
                if (! $this->isPlanningItemEqual($itemCt, $itemPrev)) {
                    return false;
                }
            }
        }
        return true;
    }

    /**
     * @param array{DTE_LIV:string,QTE_COMM:string} $itemA
     * @param array{DTE_LIV:string,QTE_COMM:string} $itemB
     */
    private function isPlanningItemEqual(array $itemA, array $itemB) :bool
    {
        return $itemA['DTE_LIV'] == $itemB['DTE_LIV']
            && floatval($itemA['QTE_COMM']) == floatval($itemB['QTE_COMM']);
    }

    /**
     * @param array{DTE_LIV:string,QTE_COMM:string} $a
     * @param array{DTE_LIV:string,QTE_COMM:string} $b
     */
    private function fnSortDate($a, $b) :int
    {
        return strcmp($a['DTE_LIV'], $b['DTE_LIV']);
    }

    // private function sendEmailUpdate()
    // {
        
    // }
}
