<?php

namespace Modules\Consultation\Service;

use Entity\Model\AoDaRep;
use Entity\Model\AoExCenv;
use Entity\Model\AoQstR;
use Entity\Model\AoRepQst;
use Helpers\Outil\ArrayHelper;
use Helpers\Phpspreadsheet\SpreadsheetHelper;
use Modules\Consultation\Validator\ImportValidator;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
use Modules\Consultation\Repository\ImportExcelRepo;
use Helpers\Trad\Trad;
use GuzzleHttp\Psr7\Response;
use Helpers\Outil\Sql;
use Entity\Model\AoLigne;
use Entity\Model\AoQstQ;
use Helpers\Log\LogSysteme;
use Helpers\Trad\ITrad;
use Skeleton\Renderer\RendererInterface;

class ImportExcelService
{

    private Itrad $trad;
    private \PDO $pdo;
    protected RendererInterface $renderer;

    public function __construct(\PDO $pdo, ITrad $trad, RendererInterface $renderer)
    {
        $this->trad         = $trad;
        $this->pdo          = $pdo;
        $this->renderer     = $renderer;
    }

    public function importConsultation(
        string $idAO,
        string $fichier,
        bool $confirm,
        ?bool $ignoreBdd99
    ) :Response {
        $trad = $this->trad;
        
        $spreadsheet = \PhpOffice\PhpSpreadsheet\IOFactory::load($fichier);
        $sheet = $spreadsheet->getSheet(0);
        $importId = (string) $sheet->getCell([1,1])->getValue();


        $repo = new ImportExcelRepo($this->pdo);
        $importInfo = $repo->getInfoImport($importId);
        
        if (empty($importInfo)) {
            return $this->createErrorResponse(
                $trad->t('Ce fichier Excel n\'a pas été identifié comme lié à une consultation générée par le portail achats Oxalys.')
            );
        }
        $tradFou = $trad->copy($importInfo['Fournisseur_COD_LNG']);

        if ($importInfo['NOAO'] != $idAO) {
            return $this->createErrorResponse(Trad::strBuild(
                $trad->t('Cette réponse ne correspond pas à la consultation N° @1@.'),
                $idAO
            ));
        }

        // Si la version du formulaire stockée dans l’envoi n’est plus disponible en BDD
        // (from AOEXCENTE where CODEFORM=AOEXCENV.FORMULAIR and VERSFORM=AOEXCENV.VERSI)
        if (! $importInfo['hasForm']) {
            return $this->createErrorResponse(Trad::strBuild(
                $trad->t('La version n°@1@ du formulaire @2@ est inexistante. Ce fichier ne peut être intégré.'),
                $importInfo['VERSI'],
                $importInfo['FORMULAIR']
            ));
        }
        
        if ($importInfo['Entet_DTE_VALID_AO'] == '00000000'
            && $importInfo['Entet_VALCHXFOU'] != '00000000'
        ) {
            throw new \Exception('Invalid AO');
        }

        //___________________
        // Parse Réponse

        $mapAoLigne = array_column(
            (new AoLigne($this->pdo))->findAll(['NO_AO' => $idAO]),
            null,
            'NO_PO'
        );

        $repo = new ImportExcelRepo($this->pdo);
        $listFormInput = $repo->getForm(
            $importInfo['FORMULAIR'],
            intval($importInfo['VERSI'])
        );
        try {
            $formDataEntet = $this->parseAoExcelEntet($sheet, $importInfo, $listFormInput);
            $formDataListLigne = $this->parseAoExcelLigne($sheet, $importInfo, $mapAoLigne);
            
            
            //___________________
            // Parse Réponse Variante

            $hasVariante = Sql::toBool($importInfo['Entet_VARIANTE']);
            $isVarianteParsed = false;
            $formDataVarianteEntet = null;
            $formDataVarianteListLigne = null;
            if ($hasVariante) {
                $sheet = $spreadsheet->getSheetByName($tradFou->t('Variante'));
                if ($sheet != null) {
                    $formDataVarianteEntet = $this->parseAoExcelEntet($sheet, $importInfo, $listFormInput);
                    $formDataVarianteListLigne = $this->parseAoExcelLigne($sheet, $importInfo, $mapAoLigne);

                    // Note : DATREP (aka AO_DAREP.DTE_REP) is the id of the "Response".
                    // Here we force it equal to the "conforme" so that
                    // they can share the reponse to the Questionnaire
                    $formDataVarianteEntet['DATREP'] = $formDataEntet['DATREP'] ?? null;

                    $isVarianteParsed = ! $this->isFormDataLigneEmpty($formDataVarianteListLigne);
                }
            }
        } catch (ImportExcelException $e) {
            return new Response(400, [], match ($e->getErrCode()) {
                'NO_PO_NOT_FOUND' => Trad::strBuild(
                    $trad->t('Numéro de ligne manquant ou non valide à la colonne @1@, ligne @2@'),
                    $e->getParam()['col'],
                    $e->getParam()['row']
                ),
                'INVALID_FORM' => Trad::strBuild(
                    $trad->t('Form "@1@" version #@2@ non-valide'),
                    $e->getParam()['form'],
                    $e->getParam()['version']
                ),
                default => 'Invalid code error #' . $e->getErrCode(),
            });
        }

        //___________________
        // Parse Questionnaire

        $sheet = $spreadsheet->getSheetByName($tradFou->t('Questionnaire'));

        $listReponseQuest = null;
        if ($sheet !== null) {
            $listReponseQuest = $this->parseQuestionnaire($sheet, $importInfo, $formDataEntet['DATREP']);
        }

        //___________________
        // Validation Réponse

        if (!$confirm) {
            $alreadyExist = (new AoDaRep($this->pdo))->find([
                'NO_AO' => $idAO,
                'CODE_FOU' => $importInfo['CODE_FOU'],
                'TYPE_FG' => $importInfo['FournisseurType'],
                'DTE_REP' => $formDataEntet['DATREP'],
                'C_V' => 'C',
            ]);
            if (!empty($alreadyExist)) {
                return $this->createJsonResponse([
                    'type' => 'confirm',
                    'popupHtml' => $this->renderer->render('@popup-erreur/simpleWarning', [
                        'content' => $trad->t('Une réponse à cet consultation existe déjà pour cette date. Souhaitez-vous la remplacer ?'),
                    ]),
                    'message_raw' => $trad->t('Une réponse à cet consultation existe déjà pour cette date. Souhaitez-vous la remplacer ?'),
                ], 400);
            }
    
            // Cas d’un envoi déjà chargé dans les réponses : AOEXCENV.RETOU=0x54
            if ($importInfo['countResponse'] != 0
                && Sql::toBool($importInfo['RETOU'])
            ) {
                if (intval($importInfo['countResponse']) < 3) {
                    return $this->createJsonResponse([
                        'type' => 'confirm',
                        'popupHtml' => $this->renderer->render('@popup-erreur/simpleWarning', [
                            'content' => $trad->t('Cette réponse a déjà été lue. Souhaitez-vous néanmoins en créer une nouvelle ?'),
                        ]),
                        'message' => $trad->t('Cette réponse a déjà été lue. Souhaitez-vous néanmoins en créer une nouvelle ?'),
                    ], 400);
                }
                return $this->createJsonResponse([
                    'type' => 'confirm',
                    'popupHtml' => $this->renderer->render('@popup-erreur/simpleWarning', [
                        'content' => $trad->t('Il existe déjà 3 réponses pour ce fournisseur. Souhaitez-vous remplacer la réponse précédente ?'),
                    ]),
                    'message_raw' => $trad->t('Il existe déjà 3 réponses pour ce fournisseur. Souhaitez-vous remplacer la réponse précédente ?'),
                ], 400);
            }
        }
        

        $validator = new ImportValidator(
            $this->pdo,
            $this->trad
        );
        
        $validator->validateFormEntet($formDataEntet, $hasVariante ? false : null);
        $validator->validateListFormLigne($importInfo, $formDataListLigne, $hasVariante ? false : null);

        if ($isVarianteParsed) {
            $validator->validateFormEntet($formDataVarianteEntet, true);
            $validator->validateListFormLigne($importInfo, $formDataVarianteListLigne, true);
        }


        if (! $validator->isValid()) {
            return $this->createErrorResponse($validator->getError());
        }



        
        //___________________
        // BDDART99 confirm

        // Case : ignoreBdd99 is undefined and a BDD99 input is -> prompt user for a confirm
        if ($ignoreBdd99 === null
            && ArrayHelper::findFirst($listFormInput, function ($input) {
                return $input['CODEINTZ'] == 'BDDART99';
            }) !== null
        ) {
            return $this->createJsonResponse([
                'type' => 'confirm_bddart99',
                'popupHtml' => $this->renderer->render(
                    '@Consultation/ImportExcel/popupConfirmBddart99',
                    [
                        'listDataLigne' => $formDataListLigne,
                    ]
                ),
                'message_raw' =>
                    $trad->t('Souhaitez-vous mettre à jour le référentiel article du fournisseur avec les données importées suivantes ?')
                    . "\r\n"
                    . implode("\r\n", array_map(function ($dataLigne) use ($trad) {
                        return $trad->t('Article')
                            . ' '
                            . $dataLigne['AoLigne']['CODE_ART']
                            . $trad->colon()
                            . $dataLigne['BDDART99'];
                    }, array_filter($formDataListLigne, function ($dataLigne) {
                        return ! empty($dataLigne['AoLigne']['CODE_ART']);
                    })))
            ], 400);
        }
        $ignoreBdd99 = $ignoreBdd99 ?? true;

        //___________________
        //

        $this->pdo->beginTransaction();
        $repo->insertAoReponse(
            $importInfo,
            $formDataEntet,
            $formDataListLigne,
            false,
            $ignoreBdd99
        );

        if ($isVarianteParsed) {
            $repo->insertAoReponse(
                $importInfo,
                $formDataVarianteEntet,
                $formDataVarianteListLigne,
                true,
                $ignoreBdd99
            );
        }

        (new AoExCenv($this->pdo))->update(['RETOU' => true], ['NOIDENT' => $importId]);

        if (!empty($importInfo['DossierConsoli_NO_DCC'])) {
            $repo->updateDossierConsol(
                $importInfo,
                $importInfo['DossierConsoli_NO_DCC']
            );
        }

        
        //___________________
        // Réponse questionnaire
        
        if ($listReponseQuest !== null) {
            // Delete previous question
            (new AoRepQst($this->pdo))->delete([
                'NO_AO' => $idAO,
                'DTE_REP' => $formDataEntet['DATREP'],
                'CODE_FOU' => $importInfo['CODE_FOU'],
                'TYPE_FG' => $importInfo['FournisseurType'],
                'C_V' => 'C',
            ]);
            $repo->multiInsert('AO_REP_QST', $listReponseQuest);
        }
        
        $this->pdo->commit();

        return new Response();
    }

    private function createErrorResponse(string $error) :Response
    {
        return new Response(400, [], $error);
    }

//_____________________________________________________________________________
// Parser

    /**
     * @param array<mixed> $importInfo
     * @param array<array<mixed>> $listFormInput as returned from ImportExcelRepo::getForm
     * @see ImportExcelRepo::getForm
     * @return array<mixed>
     */
    private function parseAoExcelEntet(
        Worksheet $sheet,
        array $importInfo,
        array $listFormInput
    ) :array {
        
        $formDataEntet = [];
        foreach ($listFormInput as $input) {
            // Case ligne
            if (! Sql::toBool($input['Dict_ENTET'])) {
                continue;
            }

            $value = $sheet->getCell([
                intval($input['NOCOL']),
                intval($input['NOLIG']),
            ])->getValue();

            if (in_array($input['CODEINTZ'], ['DATREP', 'DATLIMREP', 'DATLIMVAL'])) {
                $value = SpreadsheetHelper::parseDate($value);
            }

            $formDataEntet[ $input['CODEINTZ'] ] = trim($value);
        }

        $formDataEntet['DATREP'] = $formDataEntet['DATREP'] ?? '';
        $formDataEntet['DATLIMREP'] = $formDataEntet['DATLIMREP'] ?? '';
        
        return $formDataEntet;
    }

    /**
     * @param array<mixed> $importInfo as returned by ImportExcelRepo::getInfoImport
     * @param array<array<mixed>> $mapAoLigne map of AoLigne Entities indexed by NO_PO
     * @return array<array<mixed>>
     */
    private function parseAoExcelLigne(Worksheet $sheet, array $importInfo, array $mapAoLigne)
    {
        $repo = new ImportExcelRepo($this->pdo);
        $mapFormInput = array_column(
            $repo->getForm(
                $importInfo['FORMULAIR'],
                intval($importInfo['VERSI'])
            ),
            null,
            'CODEINTZ'
        );

        $formDataListLigne = [];
        $ligneOffset = intval($importInfo['Form_LIGUNAO']);
        $colOffset = intval($importInfo['Form_COLUNAO']);
        $hasSousTitre = !empty($importInfo['Form_NLIG_ST']);
        $ligneRowSize = intval($importInfo['Form_NBLIGAO']);
        
        $offsetSousTitre = 0;

        for ($rowGroupIndex = 0; $rowGroupIndex < count($mapAoLigne); $rowGroupIndex++) {
            $inputAoLigneId = $mapFormInput['POSTE'];
            if (empty($inputAoLigneId)) {
                throw new ImportExcelException('INVALID_FORM', [
                    'form' => $importInfo['FORMULAIR'],
                    'version' => $importInfo['VERSI'],
                ]);
            }

            // TODO : factorize
            $aoLigneId = $sheet->getCell([
                $colOffset + intval($inputAoLigneId['NOCOL']) - 1,
                $ligneOffset
                + $rowGroupIndex * $ligneRowSize
                + (intval($inputAoLigneId['NOLIG']) - 1) // convert num ligne to index using -1
                + $offsetSousTitre,
            ])->getValue();

            $aoLigne = $mapAoLigne[ $aoLigneId ] ?? null;
            if (empty($aoLigne)) {
                throw new ImportExcelException('NO_PO_NOT_FOUND', [
                    'value' => $aoLigneId,
                    'col' => $colOffset + intval($inputAoLigneId['NOCOL']) - 1,
                    'row' => $ligneOffset
                        + $rowGroupIndex * $ligneRowSize
                        + (intval($inputAoLigneId['NOLIG']) - 1) // convert num ligne to index using -1
                        + $offsetSousTitre
                ]);
            }

            $formDataListLigne[ $aoLigne['NO_PO'] ]['AoLigne'] = $aoLigne;

            if ($hasSousTitre
                && Sql::toBool($aoLigne['ST_APLIG'])
            ) {
                $offsetSousTitre++;
            }

            foreach ($mapFormInput as $input) {
                // Case : ignore entet
                if (Sql::toBool($input['Dict_ENTET'])) {
                    continue;
                }

                $value = $sheet->getCell([
                    $colOffset + intval($input['NOCOL']) - 1,
                    $ligneOffset
                    + $rowGroupIndex * $ligneRowSize
                    + (intval($input['NOLIG']) - 1) // convert num ligne to index using -1
                    + $offsetSousTitre,
                ])->getValue();
                
                $formDataListLigne[ $aoLigne['NO_PO'] ][ $input['CODEINTZ'] ] =
                    is_string($value) ? trim($value) : $value;
            }
        }
        
        return $formDataListLigne;
    }

    /**
     * @param array<mixed> $importInfo
     * @return array<array<mixed>>
     */
    private function parseQuestionnaire(Worksheet $sheet, array $importInfo, string $dateRep) :array
    {
        $idAO = $importInfo['NOAO'];
        $tradFou = $this->trad->copy($importInfo['Fournisseur_COD_LNG']);

        $mapQuestion = array_column(array_map(
            function ($row) {
                return $row + ['id' => $row['ID_THEME'] . ';' . $row['ID_QUEST'] ];
            },
            (new AoQstQ($this->pdo))->findAll([
                'NO_AO' => $idAO,
                'QUEST_FOUR' => true,
            ])
        ), null, 'id');
        $mapListChoice = ArrayHelper::indexBy(array_map(
            function ($row) {
                return $row + ['quest_id' => $row['ID_THEME'] . ';' . $row['ID_QUEST'] ];
            },
            (new AoQstR($this->pdo))->findAll(['NO_AO' => $idAO])
        ), 'quest_id');

        $listReponse = [];
        $offset = 7; // Questionnaire content start at row 7.
        $countMaxQuestionnaireSize =
            intval($importInfo['countTheme']) * 2
            + count($mapQuestion) * 2;
        foreach (range(0, $countMaxQuestionnaireSize) as $i) {
            $rowId = $offset + $i;
            $rowTag = $sheet->getCell([1,$rowId])->getValue() ?? '';

            if (!str_starts_with($rowTag, 'Q|')) {
                continue;
            }
            $a = explode('|', $rowTag);
            if (count($a) != 3) {
                LogSysteme::prefixErrorLog('Invalid row tag "'.$rowTag.'"');
                continue;
            }
            $themeId = $a[1];
            $questionId = intval($a[2]);
            $question = $mapQuestion[ $themeId . ';' . $questionId] ?? null;
            if (empty($question)) {
                LogSysteme::prefixErrorLog('Invalid row tag "'.$rowTag.'"');
                continue;
            }

            $colH = $sheet->getCell([8, $rowId])->getValue();
            $reponse = [
                'NO_AO' => $idAO,

                'DTE_REP' => $dateRep,
                'CODE_FOU' => $importInfo['CODE_FOU'],
                'TYPE_FG' => $importInfo['FournisseurType'],
                'C_V' => 'C',
                'ID_THEME' => $themeId,
                'ID_QUEST' => $questionId,
                'REP_NUM' => '',
                'REP_LOG' => false,
                'REP_TAB' => 0,
                'NM_COM' => 0,
                'REP_OUV_NM' => 0,
                'NOTE' => 0,
            ];

            switch ($question['TYPE_REP']) {
                case 'N': // Numerique
                    $reponse = [
                        'REP_NUM' => substr($colH, 0, 18),
                        'NOTE' => 0,
                    ] + $reponse;
                    break;
                case 'O': // Ouvert
                    $reponse = ['REP_OUV_NM' => Sql::updateMemo($this->pdo, '', $colH)] + $reponse;
                    break;
                case 'L': // Logique (yes / no)
                    $b = ($tradFou->t('Oui') == $colH);
                    $reponse = [
                        'REP_LOG' => $b,
                        'NOTE' => $b ? $question['NOTE_VRAI'] : $question['NOTE_VRAI'],
                    ] + $reponse;
                    break;
                case 'T': // Tableau (list de choix)
                    $choix = ArrayHelper::findFirst(
                        $mapListChoice[ $themeId . ';' . $questionId ] ?? [],
                        function ($choix) use ($colH) {
                            return $choix['TXT_REP'] == $colH;
                        }
                    );
                    $reponse = [
                        'REP_TAB' => $choix['ID_REP'] ?? 0,
                        'NOTE' => $choix['NOTE'] ?? 0,
                    ] + $reponse;
                    break;
                case 'C': // Calculé
                    // Ignored
                    break;
            }

            $listReponse[] = $reponse;
        }
        return $listReponse;
    }

    /**
     * createJsonResponse
     *
     * @todo merge with Controller::createJsonResponse
     * @param  array<mixed> $data
     * @param  int $status
     * @return Response
     */
    protected function createJsonResponse(array $data, int $status = 200) :Response
    {
        return new Response(
            $status,
            ['Content-Type' => 'application/json'],
            (string) json_encode($data)
        );
    }

    /**
     * @param array<mixed> $formDataListLigne
     */
    private function isFormDataLigneEmpty(array $formDataListLigne) :bool
    {
        // Assume filled if it has at least one Tarif (prix)
        foreach ($formDataListLigne as $ligneInput) {
            if (strlen($ligneInput['TARIF']) != 0) {
                return false;
            }
        }
        return true;
    }
}
