<?php

declare(strict_types=1);

namespace App\Module\Transport\Infrastructure\Console;

use App\Module\Transport\Application\Idtf\IdtfSheetParser;
use DateTimeImmutable;
use Doctrine\DBAL\Connection;
use PhpOffice\PhpSpreadsheet\IOFactory;
use RuntimeException;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Throwable;

use function array_slice;
use function count;
use function in_array;

use const JSON_THROW_ON_ERROR;
use const JSON_UNESCAPED_UNICODE;

/**
 * ERP-149 : synchronise le referentiel des codes IDTF (regimes de nettoyage
 * transport).
 *
 * Recupere l'export Excel depuis le generateur icrt-idtf.com (ou un fichier
 * local), le parse et synchronise `idtf_product` de facon transactionnelle :
 * upsert sur (schema, idtf_number), soft-delete des absents, journal dans
 * `idtf_sync_log`. Idempotente (refresh complet).
 */
#[AsCommand(
    name: 'app:idtf:sync',
    description: 'Synchronise le referentiel des codes IDTF depuis l\'export Excel icrt-idtf.com (upsert + soft-delete + journal).',
)]
final class SyncIdtfCommand extends Command
{
    private const string GENERATOR_URL = 'https://www.icrt-idtf.com/fr/excel-generator/';

    /**
     * Champs a cocher explicitement : `fields[]=all` ne deplie PAS les colonnes
     * cote serveur (6 colonnes seulement). Cette liste donne l'export complet
     * (11 colonnes). Cf. ERP-149 § 1.
     *
     * @var list<string>
     */
    private const array EXPORT_FIELDS = [
        'product_number_idtf',
        'product_name',
        'minimum_cleaning_regime',
        'important_requirements',
        'date_mandatory',
        'related_products',
        'formula',
        'product_number_eural',
        'product_number_cas',
        'footnotes',
    ];

    public function __construct(
        private readonly Connection $connection,
        private readonly HttpClientInterface $httpClient,
    ) {
        parent::__construct();
    }

    protected function configure(): void
    {
        $this
            ->addOption('schema', null, InputOption::VALUE_REQUIRED, "Module IDTF : 'road' (routier) ou 'water' (fluvial).", 'road')
            ->addOption('file', null, InputOption::VALUE_REQUIRED, "Chemin d'un .xlsx local (court-circuite le telechargement, utile pour tests/rejeu).")
            ->addOption('dry-run', null, InputOption::VALUE_NONE, 'Analyse sans ecriture en base.')
        ;
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $io     = new SymfonyStyle($input, $output);
        $schema = (string) $input->getOption('schema');
        $dryRun = (bool) $input->getOption('dry-run');
        $file   = $input->getOption('file');

        if (!in_array($schema, ['road', 'water'], true)) {
            $io->error("--schema doit valoir 'road' ou 'water'.");

            return Command::INVALID;
        }

        // 1. Recuperation du binaire xlsx (local ou via POST).
        try {
            $xlsx = null !== $file ? $this->readLocal((string) $file) : $this->downloadExport($schema);
        } catch (Throwable $e) {
            $io->error('Telechargement/lecture impossible : '.$e->getMessage());

            return Command::FAILURE;
        }

        // 2. Parsing (xlsx -> matrice -> lignes normalisees).
        try {
            $parsed = IdtfSheetParser::parse($this->toMatrix($xlsx));
        } catch (Throwable $e) {
            $io->error('Parsing impossible : '.$e->getMessage());

            return Command::FAILURE;
        }

        $rows       = $parsed['rows'];
        $exportDate = $parsed['exportDate'] ?? new DateTimeImmutable()->format('Y-m-d');

        $io->section(sprintf('IDTF %s — export du %s', mb_strtoupper($schema), $exportDate));
        $io->writeln(sprintf('%d lignes exploitables lues.', count($rows)));

        if ($dryRun) {
            $this->renderPreview($io, $rows);
            $io->note(sprintf('Dry-run : aucune ecriture. (%d lignes au total)', count($rows)));

            return Command::SUCCESS;
        }

        // 3. Sync transactionnelle : upsert -> soft-delete -> journal.
        $run = new DateTimeImmutable()->format('Y-m-d H:i:s.u');

        $this->connection->beginTransaction();

        try {
            $upserted    = $this->upsertAll($schema, $exportDate, $rows, $run);
            $deactivated = $this->deactivateMissing($schema, $run);
            $this->log($schema, $exportDate, count($rows), $upserted, $deactivated);
            $this->connection->commit();
        } catch (Throwable $e) {
            $this->connection->rollBack();
            $io->error('Sync annulee (rollback) : '.$e->getMessage());

            return Command::FAILURE;
        }

        $io->success(sprintf('%d upsert, %d desactive(s).', $upserted, $deactivated));

        return Command::SUCCESS;
    }

    /**
     * Rejoue le POST du generateur pour recuperer le binaire xlsx complet.
     * Le formulaire poste sur lui-meme ; pas besoin de GET/cookies prealables.
     */
    private function downloadExport(string $schema): string
    {
        // Corps construit a la main : http-client encoderait fields[] en
        // indices numerotes, on veut bien des "fields[]=..." repetes.
        $pairs = [
            'schema='.$schema,
            'type%5B%5D='.$schema,
            'roadRegime%5B%5D=all',
            'waterRegime%5B%5D=all',
            'groups%5B%5D=all',
            'products%5B%5D=all',
        ];

        foreach (self::EXPORT_FIELDS as $field) {
            $pairs[] = 'fields%5B%5D='.$field;
        }

        $pairs[] = 'generateExcel=';

        $response = $this->httpClient->request('POST', self::GENERATOR_URL, [
            'headers' => ['Content-Type' => 'application/x-www-form-urlencoded'],
            'body'    => implode('&', $pairs),
            'timeout' => 90,
        ]);

        $content     = $response->getContent();
        $contentType = $response->getHeaders(false)['content-type'][0] ?? '';

        // Garde-fou : un HTML signifie un POST rejete (filtres/payload).
        if (!str_contains($contentType, 'spreadsheet') && !str_starts_with($content, "PK\x03\x04")) {
            throw new RuntimeException(sprintf('Reponse non-xlsx (content-type: %s). Verifie le payload.', $contentType));
        }

        return $content;
    }

    private function readLocal(string $path): string
    {
        $raw = @file_get_contents($path);

        if (false === $raw) {
            throw new RuntimeException(sprintf('Fichier illisible : %s', $path));
        }

        return $raw;
    }

    /**
     * Charge le binaire xlsx via PhpSpreadsheet et retourne la feuille active
     * sous forme de matrice 0-indexee (lignes/colonnes).
     *
     * @return array<int, array<int, mixed>>
     */
    private function toMatrix(string $xlsx): array
    {
        $tmp = tempnam(sys_get_temp_dir(), 'idtf_').'.xlsx';
        file_put_contents($tmp, $xlsx);

        try {
            // toArray(null, true, true, false) : colonnes 0-indexees.
            return IOFactory::load($tmp)->getActiveSheet()->toArray(null, true, true, false);
        } finally {
            @unlink($tmp);
        }
    }

    /**
     * Upsert de toutes les lignes (cle naturelle = schema + idtf_number).
     *
     * @param list<array<string, mixed>> $rows
     */
    private function upsertAll(string $schema, string $exportDate, array $rows, string $run): int
    {
        $sql = <<<'SQL'
            INSERT INTO idtf_product
                (idtf_number, schema, product_group, name, cleaning_regime, important_requirements,
                 mandatory_date, related_products, formula, eural_code, cas_numbers, footnotes,
                 source_export_date, is_active, last_synced_at)
            VALUES
                (:idtf, :schema, :grp, :name, :regime, :req, :mdate, :related, :formula, :eural,
                 CAST(:cas AS JSONB), :foot, :export, TRUE, :run)
            ON CONFLICT (schema, idtf_number) DO UPDATE SET
                product_group          = EXCLUDED.product_group,
                name                   = EXCLUDED.name,
                cleaning_regime        = EXCLUDED.cleaning_regime,
                important_requirements = EXCLUDED.important_requirements,
                mandatory_date         = EXCLUDED.mandatory_date,
                related_products       = EXCLUDED.related_products,
                formula                = EXCLUDED.formula,
                eural_code             = EXCLUDED.eural_code,
                cas_numbers            = EXCLUDED.cas_numbers,
                footnotes              = EXCLUDED.footnotes,
                source_export_date     = EXCLUDED.source_export_date,
                is_active              = TRUE,
                last_synced_at         = EXCLUDED.last_synced_at
            SQL;

        $count = 0;

        foreach ($rows as $r) {
            $this->connection->executeStatement($sql, [
                'idtf'    => $r['idtf_number'],
                'schema'  => $schema,
                'grp'     => $r['product_group'],
                'name'    => $r['name'],
                'regime'  => $r['cleaning_regime'],
                'req'     => $r['important_requirements'],
                'mdate'   => $r['mandatory_date'],
                'related' => $r['related_products'],
                'formula' => $r['formula'],
                'eural'   => $r['eural_code'],
                'cas'     => json_encode($r['cas_numbers'], JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR),
                'foot'    => $r['footnotes'],
                'export'  => $exportDate,
                'run'     => $run,
            ]);
            ++$count;
        }

        return $count;
    }

    /**
     * Soft-delete : toute ligne du schema active non revue par ce run passe a
     * is_active=false.
     */
    private function deactivateMissing(string $schema, string $run): int
    {
        return (int) $this->connection->executeStatement(
            'UPDATE idtf_product SET is_active = FALSE WHERE schema = :schema AND is_active = TRUE AND last_synced_at < :run',
            ['schema' => $schema, 'run' => $run],
        );
    }

    private function log(string $schema, string $exportDate, int $total, int $upserted, int $deactivated): void
    {
        $this->connection->executeStatement(
            <<<'SQL'
                INSERT INTO idtf_sync_log (schema, export_date, rows_total, rows_upserted, rows_deactivated)
                VALUES (:schema, :export, :total, :upserted, :deactivated)
                SQL,
            [
                'schema'      => $schema,
                'export'      => $exportDate,
                'total'       => $total,
                'upserted'    => $upserted,
                'deactivated' => $deactivated,
            ],
        );
    }

    /**
     * @param list<array<string, mixed>> $rows
     */
    private function renderPreview(SymfonyStyle $io, array $rows): void
    {
        $io->table(
            ['IDTF', 'Nom', 'Regime', 'CAS'],
            array_map(static fn (array $r): array => [
                (string) $r['idtf_number'],
                mb_strimwidth((string) $r['name'], 0, 50, '…'),
                (string) $r['cleaning_regime'],
                implode(', ', $r['cas_numbers']),
            ], array_slice($rows, 0, 15)),
        );
    }
}
