<?php

namespace Modules\Facture\Service;

use API\ChorusPro\ChorusPartenaire;
use API\Edicom\EdicomBatch;
use API\Edicom\EdicomFactureBatch;
use API\Edicom\EdicomPartenaire;
use API\Partenaire\IBatch;
use API\Partenaire\IFactDematReceiver;
use API\Partenaire\Service\PartenaireService;
use DateTime;
use Entity\Model\Enum\EFacDematFlowStatus;
use Entity\Model\Enum\EFacDematStatusReason;
use Entity\Model\FacCdeLad;
use Entity\Model\FacDematFlow;
use Entity\Model\FacLig;
use Entity\Model\FacLigTravail;
use Entity\Model\FacReg;
use Entity\Model\FactEnt;
use Entity\Model\Fourniss;
use Entity\Model\Metadata\EntityMetaQb;
use Entity\Model\Metadata\FacDematBatchData;
use Entity\Model\Metadata\FacDematMappage;
use Entity\Model\Paravrac;
use Exception;
use Helpers\Fichier\Gestion_Fic_PJ;
use Helpers\Log\LogSysteme;
use Helpers\Outil\ArrayHelper;
use Helpers\Outil\BcMathNumber as BcMath;
use Helpers\Outil\Date;
use Helpers\Outil\Sql;
use Helpers\Outil\XmlHelper;
use Helpers\Outil\XPathHelper;
use Helpers\Parametres\Constante;
use Helpers\QueryBuilder\SqlExpr;
use Helpers\Trad\ITrad;
use Helpers\Validator\FormValidator;
use Modules\Facture\Repository\CreationFacture;
use Modules\Facture\Repository\FactureDematRepo;
use Modules\Facture\Repository\PartnDematRepo;
use Modules\Facture\WebService\CreationFacture as WebServiceCreationFacture;
use Psr\Container\ContainerInterface;

/**
 * @phpstan-import-type Flow from \API\FactDematDefault\FactDematDefaultPartenaire
 * @phpstan-import-type SearchResult from \API\FactDematDefault\FactDematDefaultPartenaire
 */
class FactureDematBatch implements IBatch
{
    private ContainerInterface $container;
    private \PDO               $pdo;
    private string             $numeroTraitement;

    public function __construct(ContainerInterface $container)
    {
        $this->container = $container;
        $this->pdo       = $container->get(\PDO::class);
    }

    protected function getPdo(): \PDO
    {
        return $this->pdo;
    }

    public function getTranslationService(): ITrad
    {
        return $this->container->get(ITrad::class);
    }

    public function getFacDematRepo(): FactureDematRepo
    {
        return $this->container->get(FactureDematRepo::class);
    }

    public function createFormValidator(): FormValidator
    {
        return new FormValidator(
            $this->pdo,
            $this->getTranslationService(),
            $this->container,
        );
    }

    public function batch(string $numeroTraitement): string
    {
        $this->numeroTraitement = $numeroTraitement;
        $logger                 = new \Helpers\Log\OxaEventDet($this->pdo, $numeroTraitement);

        $listPartn = (new PartnDematRepo($this->pdo))->listPartenaire([
            'filter_isActive' => 1,
        ]);

        $countError = 0;
        foreach ($listPartn as &$partn) {
            $ws = (new PartenaireService($this->container))
                ->getFactDematPartn($partn);

            try {
                // TODO : refacto
                if ($ws instanceof EdicomPartenaire) {
                    (new EdicomFactureBatch($this->container, $ws))
                        ->batch($this->numeroTraitement);
                    continue;
                }

                $stat          = $this->processPartn($ws);
                $partn['stat'] = $stat;
            } catch (\Exception $e) {
                $logger->setLog('E', $e->getMessage());
                LogSysteme::prefixErrorLog((string) $e);
                $countError++;
            }
        }
        unset($partn);

        /** @var int $countError */
        $countError += array_reduce(
            $listPartn,
            fn($carry, $partn) => $carry + ($partn['stat']->failed ?? 0),
            0,
        );

        $logger->setLog(
            $countError ? 'E' : 'O',
            'Récupération Facture ; ' . count($listPartn) . 'Partenaire processed ; ' . $countError . ' flow error',
            implode(
                PHP_EOL,
                array_map(
                    fn($partn) =>
                    'Partenaire ' . $partn['LABEL'] . ' (#' . $partn['ID'] . ')'
                    . ' ; processed: ' . ($partn['stat']->processed ?? 0)
                    . ' ; failed: ' . ($partn['stat']->failed ?? 0),
                    $listPartn,
                ),
            ),
        );
        return $countError ? 'E' : 'O';
    }

    private FactureDematMapper $mapper;

    public function getMapper(): FactureDematMapper
    {
        return $this->mapper = $this->mapper ?? FactureDematMapper::createFromConfig($this->container);
    }

    /**
     * @return object{processed:int,failed:int}
     */
    private function processPartn(IFactDematReceiver $partn): object
    {
        $processStart = new \DateTime();
        $qb           = new EntityMetaQb($this->pdo);
        $batchState   =
            $qb->findByEntityByStruct(FactureDematRepo::class, (string) $partn->getId(), FacDematBatchData::class)[0]
            ?? new FacDematBatchData((string) $partn->getId());

        $countSuccess = 0;
        $countFailure = 0;
        $dateLimit    = $batchState->getLastProcessDate();

        // Note : we requiere the flow to be sorted by "submittedAt" ascending,
        // since there is no normalisation to the order the API gave them to us,
        // we must read all then sort them out
        $ctx = [
            'dateLimit' => $dateLimit,
            'isLast' => false,
        ];
        $listFlow = [];
        while (true) {
            $listResult = $partn->listNext($ctx);
            $listFlow = array_merge($listFlow, $listResult);

            // Case : last page -> stop reading
            if (empty($listResult) || $ctx['isLast']) {
                break;
            }
        }
        usort($listFlow, fn($itemA, $itemB) => $itemA->submittedAt <=> $itemB->submittedAt);

        // Processing
        foreach ($listFlow as $flow) {
            try {
                switch ($flow->flowSyntax ?? 'UBL') {
                    case 'CDAR':
                        // Filter : Ignore CDAR going toward the supplier
                        if ($flow->flowDirection != 'In') {
                            continue 2;
                        }
                        $flowId = $this->processFlowCdar($partn, $flow);
                        break;
                    case 'UBL':
                    case 'ChorusFacture':
                        // Filter : Ignore Facture going toward the supplier
                        if ($flow->flowDirection != "In") {
                            continue 2;
                        }
                        $flowId = $this->processFlowUbl($partn, $flow);
                        break;
                    default:
                        $flowId = null;
                }

                if ($flowId === null) {
                    $countFailure++;
                } else {
                    $countSuccess++;
                }
            } catch (\Exception $e) {
                $countFailure++;
                LogSysteme::prefixErrorLog((string) $e);
            }
        }



        //__________

        // Case : all flow were processed correctly -> move the date forward to ignore them next batch
        if ($countFailure == 0) {
            $batchState->setLastProcessDate($processStart);
            $qb->persist([$batchState]);
        }

        return (object) [
            'processed' => $countSuccess,
            'failed'    => $countFailure,
        ];
    }

    /**
     * @param Flow $flow
     */
    public function processFlowUbl(IFactDematReceiver $partn, object $flow): ?string
    {
        $duplicate = (new FacDematFlow($this->pdo))->find([
            'PARTN_ID' => $partn->getId(),
            'FLOW_ID'  => $flow->flowId,
        ]);
        if ($duplicate) {
            // // Case : flow has been updated
            // if (new DateTime($duplicate['UPDATED']) < new DateTime($flow->updatedAt)) {
            //     throw new \Exception('Flow '.$flow->flowId.' has been updated and can not be reprocessed');
            // }
            // Ignore duplicate
            return $flow->flowId;
        }

        $qb = new EntityMetaQb($this->pdo);
        try {
            // Parse UBL
            $flowEntity = $flow->flowSyntax == 'UBL'
                ? $this->createFlowEntityFromUBL($partn, $flow)
                : $this->createFlowEntityFromChorus($partn, $flow);

            $this->pdo->beginTransaction();

            $qbFlow = new FacDematFlow($this->pdo);
            $qbFlow->insert(ArrayHelper::filterByKey(
                $flowEntity = ($flowEntity + [
                    'ID'          => $qbFlow->getNextId(),
                    'PARTN_ID'    => $partn->getId(),
                    'FLOW_ID'     => $flow->flowId,
                    'STATUS'      => EFacDematFlowStatus::MiseADispo->value,
                    'NUM_FACT'    => '0',
                    // 'FIC_UBL'     => $pathUBL,
                    // 'FIC_LISIBLE' => $pathLisible ?? '',
                    'SUBMITTED'   => (new DateTime($flow->submittedAt))->format('Ymd'),
                    'UPDATED'     => (new DateTime($flow->updatedAt))->format('Ymd'),
                    'CREATED'     => date('Ymd'),
                ]),
                [
                    'ID', 'PARTN_ID', 'FLOW_ID', 'STATUS', 'NUM_FACT',
                    'FIC_UBL', 'FIC_LISIBLE', 'SUBMITTED', 'UPDATED', 'CREATED'
                ]
            ));
            $mappage = $flowEntity['mappage'];
            unset($flowEntity['mappage']);

            $qb->persist([new FacDematMappage($flowEntity['ID'], $mappage)]);
            if (! $this->pdo->commit()) {
                return null;
            }

            //________________________________
            // Refus automatique

            $paravrac = new Paravrac($this->pdo);
            $config   = $paravrac->getMap('FAC_DEMAT');

            // Case : missing Commande and Fourniss is not in the list of exception
            if (Sql::toBool($config['REFUS_AUTO_BADCDE'])
                && empty($mappage['CdeLad'])
                && !in_array($mappage['CODE_FOUR'], $paravrac->getMap('FE_FOU_EXC'))
            ) {
                $qbFlow->updateStatus(
                    $flowEntity['ID'],
                    EFacDematFlowStatus::Refuse,
                    EFacDematStatusReason::CmdErr->value,
                    EFacDematStatusReason::CmdErr->getLabel()
                );
                return $flow->flowId;
            }

            // Case : duplicate
            $repoCreationFacture = new CreationFacture($this->pdo);
            $duplicate           = $repoCreationFacture->getFactureByRefFour(
                $mappage['FACT_FOU'],
                substr($mappage['DATE_FAFO'], 0, 4),
            );
            if (Sql::toBool($config['REFUS_AUTO_DUPLI']) && !empty($duplicate)) {
                $qbFlow->updateStatus(
                    $flowEntity['ID'],
                    EFacDematFlowStatus::Refuse,
                    EFacDematStatusReason::Doublon->value,
                    EFacDematStatusReason::Doublon->getLabel()
                );
                return $flow->flowId;
            }

            //________________________________
            // Duplicate merging

            if (!empty($duplicate)/* and not REFUS_AUTO_DUPLI */) {
                $this->pdo->beginTransaction();

                // Turn the Facture into FE
                (new FactEnt($this->pdo))->update(
                    ['ISSUE_FE' => true],
                    ['NUM_FACT' => $duplicate['NUM_FACT']],
                );

                // Attach the flow to the previous fac_ent
                $qbFlow->update(
                    ['NUM_FACT' => $duplicate['NUM_FACT']],
                    $flowEntity['ID'],
                );

                $status = EFacDematFlowStatus::calcFacStatus($duplicate);
                // Case : the Facture has already lived -> update the Flow status
                if ($status != EFacDematFlowStatus::MiseADispo) {
                    $qbFlow->updateStatus(
                        $flowEntity['ID'],
                        $status,
                        $status == EFacDematFlowStatus::Refuse
                        ? EFacDematStatusReason::Autre->value
                        : null,
                        EFacDematStatusReason::Autre->getLabel()
                    );
                }

                /** @phpstan-ignore-next-line */
                if (! $this->pdo->commit()) {
                    return null;
                }

                return $flow->flowId;
            }

            //________________________________
            // Validate mappage into a FAC_ENT

            $validator = $this->createFormValidator();
            $mappage   = $validator->validateForm(
                $this->container->get(FactEntForm::class)->build(),
                $mappage,
            );

            if ($validator->isValid()) {
                $reponse = WebServiceCreationFacture::convertToFacEnt($flowEntity, $mappage + [
                    'ISSUE_FE' => 'T',
                ]);

                if ($reponse->Statut != 'O') {
                    // Note : The have failed validation but the flow has still been processed successfully
                    return $flow->flowId;
                }

                $this->pdo->beginTransaction();

                // Mark the Flow as 'Resolved'
                //$qb->persist([new FacDematMappage($flowEntity['ID'], $mappage)]);
                (new FacDematFlow($this->pdo))->update([
                    'NUM_FACT' => $numFact = $reponse->NO_FACT,
                ], $flowEntity['ID']);

                /** @phpstan-ignore-next-line */
                if (! $this->pdo->commit()) {
                    return null;
                }

                $this->processLigCde($numFact);
                $this->processPrepaidAmount($numFact, $mappage);
            }
        } catch (\Exception $e) {
            LogSysteme::prefixErrorLog((string) $e);
            return null;
        }

        return $flow->flowId;
    }

    private function processLigCde(string $numFact): void
    {
        $numCde = (new FacCdeLad($this->pdo))
            ->find(['NUM_FACT' => $numFact])['NUM_CDE'] ?? null;
        if (!$numCde) {
            return;
        }
        $reponse = new \SimpleXMLElement(
            WebServiceCreationFacture::choixDesCommandes($numFact, [
                'listSelectionCde' => $numCde,
            ])->responseData,
        );
        if ($reponse->Statut != 'O') {
            LogSysteme::prefixErrorLog('Invalid WSDL response : ' . $reponse->asXML());
            return;
        }

        $facEnt = (new FactEnt($this->pdo))->find($numFact);
        $sumLig = (new CreationFacture($this->pdo))->sumLigTravailMontant($numFact);
        // Case : Total does not match -> do not import cde lig
        if (!BcMath::create($sumLig)->isEquals($facEnt['TOTAL_HT'])) {
            return;
        }

        // Copy FacLigTravail into FacLig
        $this->pdo->beginTransaction();
        $listLig  = (new FacLigTravail($this->pdo))->findAll(['NUM_FACT' => $numFact]);
        $qbFacLig = new FacLig($this->pdo);
        foreach ($listLig as $lig) {
            $qbFacLig->insert(
                ArrayHelper::filterByKey($lig, FacLig::listField()),
            );
        }

        $this->pdo->commit();
    }

    /**
     * @param array<mixed> $mappage
     */
    private function processPrepaidAmount(string $numFact, array $mappage): void
    {
        $facEnt = (new FactEnt($this->pdo))->find($numFact);

        // Case : not applicable
        if (empty($mappage['PrepaidAmount'])) {
            return;
        }

        $this->pdo->beginTransaction();

        (new FacReg($this->pdo))->insert([
            'NUM_FACT'   => $facEnt['NUM_FACT'],
            'NUM_REG'    => 1,
            'REF_REG'    => 'FE_1',
            'DATE_REG'   => $facEnt['DATE_FAFO'],
            'MODE_REG'   => $facEnt['MODE_REG'],
            'MT_REG'     => $mappage['PrepaidAmount'],
            'MT_REG_DEV' => $mappage['PrepaidAmount'],
            'DEVISE'     => $facEnt['DEVISE'],
            'TYP_MT'     => 'C',
        ]);

        (new FactEnt($this->pdo))->update([
            'DTE_DERNREG' => $facEnt['DATE_FAFO'],
            'MT_TOTREG'   => $mappage['PrepaidAmount'],
        ], $facEnt['NUM_FACT']);

        $this->pdo->commit();
    }

    /**
     * @param Flow $flow
     */
    protected function processFlowCdar(IFactDematReceiver $partn, object $flow): string
    {
        $doc = new \DOMDocument();
        $b   = $doc->loadXML($partn->getCdarFile($flow->flowId)->getContents());
        if ($b == false) {
            throw new \Exception('Unreadable flow #' . $flow->flowId);
        }

        try {
            $listStatusUpdate = $this->parseCdar($doc);
        } catch (\Exception $e) {
            // Re-throw with more info
            throw new \Exception('Invalid flow #' . $flow->flowId . ': ' . $e->getMessage());
        }

        $this->processStatusUpdate($partn, $listStatusUpdate);


        return $flow->flowId;
    }

    /**
     * @param Flow $flow
     * @return array<mixed>
     */
    protected function createFlowEntityFromUBL(IFactDematReceiver $partn, object $flow) :array
    {
        $fileManager = new Gestion_Fic_PJ($this->pdo);

        $pathUBL = $fileManager->insertFichier(
            'partn_' . $partn->getId() . '_flow_' . $flow->flowId . '_ubl.xml',
            $ublStream = $partn->getFlowFile($flow->flowId)
        );

        if ($pathUBL === false) {
            throw new \Exception('insertFichier failed');
        }

        $streamReadbleFile = $partn->getReadableViewFile($flow);
        $pathLisible       = $streamReadbleFile ? $fileManager->insertFichier(
            'partn_' . $partn->getId() . '_flow_' . $flow->flowId . '_lisible.pdf',
            $streamReadbleFile,
        ) : null;
        if ($pathLisible === false) {
            throw new \Exception('insertFichier failed');
        }

        //________________________________
        // Parse flow into mappage

        $doc = new \DOMDocument();
        $b   = $doc->loadXML($ublStream->getContents());
        if ($b === false) {
            throw new \Exception('Unparsable UBL for flow "' . $flow->flowId . '"');
        }
        return [
            'FIC_UBL'     => $pathUBL,
            'FIC_LISIBLE' => $pathLisible ?? '',
            'mappage' => $this->getMapper()->map($doc, $this->pdo),
        ];
    }

    /**
     * @param Flow $flow
     * @return array<mixed>
     */
    protected function createFlowEntityFromChorus(IFactDematReceiver $partn, object $flow) :array
    {
        // Fail safe
        if (! ($partn instanceof ChorusPartenaire)) {
            throw new Exception('Not a chorus partenaire');
        }

        $streamReadbleFile = $partn->getReadableViewFile($flow);
        $pathLisible       = $streamReadbleFile ? (new Gestion_Fic_PJ($this->pdo))->insertFichier(
            'partn_' . $partn->getId() . '_flow_' . $flow->flowId . '_lisible.pdf',
            $streamReadbleFile,
        ) : null;
        if ($pathLisible === false) {
            throw new \Exception('insertFichier failed');
        }

        return [
            'FIC_LISIBLE' => $pathLisible ?? '',
            'mappage' => $partn->getMappageChorus($flow->flowId, $this->pdo),
        ];
    }

    /**
     * @param array{factureId:string,statusCode:string,factureIssueDate:string}[] $listStatusUpdate
     */
    protected function processStatusUpdate(
        IFactDematReceiver $partn,
        array $listStatusUpdate,
    ): void {

        $qbFlow   = new FacDematFlow($this->pdo);
        $qbFac    = new FactEnt($this->pdo);
        $qbFacReg = new FacReg($this->pdo);
        foreach ($listStatusUpdate as $statusUpdate) {
            // Ignore other status
            if ($statusUpdate['statusCode'] != EFacDematFlowStatus::Encaisse->value) {
                continue;
            }

            $flowFacture = $this->getFacDematRepo()->listFlow([
                'filter_egal_PARTN_ID' => $partn->getId(),
                'filter_egal_FACT_FOU' => $statusUpdate['factureId'],
                'filter_dateIssueYear' => substr($statusUpdate['factureIssueDate'], 0, 4),
            ])[0] ?? null;

            // Case : not found -> ignore
            if (empty($flowFacture)) {
                continue;
            }
            
            
            //_________
            // Process status "Encaissé"
            
            $this->pdo->beginTransaction();
            
            $flowFacture['STATUS'] = $statusUpdate['statusCode'];
            $qbFlow->updateEntity(ArrayHelper::filterByKey($flowFacture, [
                'ID', 'STATUS',
            ]));

            $fac = (new FactEnt($this->pdo))->find($flowFacture['NUM_FACT']);
            if (! empty($fac)) {
                // Update FAC_REG
                $sum = $qbFacReg->sum(
                    ['MT_REG'],
                    ['NUM_FAC' => $fac['NUM_FACT']]
                );
                $remaining = BcMath::create($fac['TOTAL_TTC'])
                    ->sub($sum['MT_REG'], Constante::montantDecimal)
                    ->getValue();
    
                if ($remaining > 0) {
                    $qbFac->update([
                        'MT_TOTREG'   => $fac['TOTAL_TTC'],
                        'DTE_DERNREG' => date('Ymd'),
                    ], ['NUM_FACT' => $fac['NUM_FACT']]);
    
                    $qbFacReg->insert([
                        'NUM_FAC'    => $fac['NUM_FACT'],
                        'REF_REG'    => 'FE_' . date('Y-m-d'),
                        'DTE_REG'    => date('Ymd'),
                        'MODE_REG'   => $fac['MODE_REG'],
                        'MT_REG'     => $remaining,
                        'MT_REG_DEV' => $remaining,
                        'DEVISE'     => $fac['DEVISE'],
                        'TYP_MT'     => 'C',
                    ]);
                }
            }


            $this->pdo->commit();
        }
    }

    //______________________________________________________________________________
// Sub-routine

    /**
     * @return array{factureId:string,statusCode:string,factureIssueDate:string}[]
     */
    public function parseCdar(\DOMDocument $doc)
    {
        $xpath = new XPathHelper($doc, null, [
            'ram' => 'urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100',
            'rsm' => 'urn:un:unece:uncefact:data:standard:CrossDomainAcknowledgementAndResponse:100',
            'qdt' => 'urn:un:unece:uncefact:data:standard:QualifiedDataType:100',
        ]);
        
        $listRefDoc = iterator_to_array(
            $xpath->mustQuery('./rsm:AcknowledgementDocument/ram:ReferenceReferencedDocument'),
        );

        $tmp = [];
        foreach ($listRefDoc as $refDoc) {
            $xpath->setContext($refDoc);
            $factureId  = $xpath->queryFirstValue('./ram:IssuerAssignedID');
            $statusCode = $xpath->queryFirstValue('./ram:ProcessConditionCode');

            $nodeDate = $xpath->mustQuery('./ram:FormattedIssueDateTime/qdt:DateTimeString')->item(0);
            if (empty($nodeDate)) {
                throw new \Exception('Missing date');
            }

            $date = match ($nodeDate->attributes->getNamedItem('format')->nodeValue) {
                '102' /* Ymd */ ,
                '203' /* YmdHi */ ,
                '204' /* YmdHis */ ,
                '303' /* YmdHiz */ ,
                '304' /* YmdHisz */ ,
                  => substr($nodeDate->textContent, 0, 8),
                default => null,
            };

            if (empty($factureId) || empty($statusCode) || empty($date)) {
                throw new \Exception('Invalid data');
            }

            $tmp[] = [
                'factureId'        => $factureId,
                'statusCode'       => $statusCode,
                'factureIssueDate' => $date,
            ];
        }
        return $tmp;
    }

    // private function parseFieldSyntaxe(string $str) :string
    // {
    //     switch ($str) {
    //         case 'UBL':
    //         case 'CII':
    //         case 'CDAR':
    //         case 'FRR':
    //             return $str;
    //         case 'Factur-X':
    //             return 'FacX';
    //     }
    //     throw new Exception('Invalid syntaxe "'.$str.'"');
    // }

    // private function parseFieldProfile(string $str) :string
    // {
    //     switch ($str) {
    //         case 'Basic':
    //             return 'BAS';
    //         case 'CIUS':
    //             return 'CIUS';
    //         case 'Extended-CTC-FR':
    //             return 'CTCF';
    //     }
    //     throw new Exception('Invalid profile "'.$str.'"');
    // }
}
