<?php

declare(strict_types=1);

namespace App\Module\Transport\Application\Idtf;

use RuntimeException;

use function array_slice;

/**
 * Parsing pur d'une matrice (lignes/colonnes 0-indexees, telle que retournee
 * par PhpSpreadsheet::toArray) de l'export Excel IDTF vers des lignes
 * normalisees pretes a l'upsert. Sans dependance a PhpSpreadsheet : la matrice
 * est un simple tableau, ce qui rend le parsing testable en isolation.
 *
 * Robuste au reordonnancement des colonnes (mapping par libelle normalise) et
 * aux lignes de preambule (detection dynamique de la ligne d'en-tete). Voir
 * ERP-149 § 2.
 */
final class IdtfSheetParser
{
    /**
     * @param array<int, array<int, mixed>> $matrix
     *
     * @return array{exportDate: null|string, rows: list<array<string, mixed>>}
     */
    public static function parse(array $matrix): array
    {
        $exportDate  = self::extractExportDate($matrix);
        $headerIndex = self::findHeaderIndex($matrix);

        if (null === $headerIndex) {
            throw new RuntimeException("Ligne d'en-tete introuvable (colonne 'Numero IDTF').");
        }

        $map = self::buildColumnMap($matrix[$headerIndex]);

        if (!isset($map['idtf_number'])) {
            throw new RuntimeException("Colonne 'Numero IDTF' introuvable dans l'en-tete.");
        }

        $rows = [];

        foreach (array_slice($matrix, $headerIndex + 1) as $row) {
            $idtf = trim((string) ($row[$map['idtf_number']] ?? ''));

            // Ligne vide / non exploitable : pas d'identifiant numerique.
            if ('' === $idtf || !ctype_digit($idtf)) {
                continue;
            }

            $rows[] = [
                'idtf_number'            => (int) $idtf,
                'product_group'          => self::val($row, $map['product_group'] ?? null),
                'name'                   => self::val($row, $map['name'] ?? null) ?? '',
                'cleaning_regime'        => self::val($row, $map['cleaning_regime'] ?? null) ?? '',
                'important_requirements' => self::val($row, $map['important_requirements'] ?? null),
                'mandatory_date'         => self::parseDate(self::val($row, $map['mandatory_date'] ?? null)),
                'related_products'       => self::val($row, $map['related_products'] ?? null),
                'formula'                => self::val($row, $map['formula'] ?? null),
                'eural_code'             => self::val($row, $map['eural_code'] ?? null),
                'cas_numbers'            => self::splitCas(self::val($row, $map['cas'] ?? null)),
                'footnotes'              => self::val($row, $map['footnotes'] ?? null),
            ];
        }

        return ['exportDate' => $exportDate, 'rows' => $rows];
    }

    /**
     * Cherche une date "d-m-Y" dans les premieres lignes (preambule
     * "Export date: 12-6-2026") et la convertit en "Y-m-d". Null si absente.
     *
     * @param array<int, array<int, mixed>> $matrix
     */
    public static function extractExportDate(array $matrix): ?string
    {
        foreach (array_slice($matrix, 0, 5) as $row) {
            $line = implode(' ', array_map(static fn (mixed $c): string => (string) $c, $row));

            if (preg_match('/(\d{1,2})-(\d{1,2})-(\d{4})/', $line, $m)) {
                $day   = (int) $m[1];
                $month = (int) $m[2];
                $year  = (int) $m[3];

                if (checkdate($month, $day, $year)) {
                    return sprintf('%04d-%02d-%02d', $year, $month, $day);
                }
            }
        }

        return null;
    }

    /**
     * Index de la ligne d'en-tete : premiere ligne contenant une cellule dont
     * le libelle normalise contient "numero idtf".
     *
     * @param array<int, array<int, mixed>> $matrix
     */
    private static function findHeaderIndex(array $matrix): ?int
    {
        foreach ($matrix as $i => $row) {
            foreach ($row as $cell) {
                if (str_contains(self::normalize((string) $cell), 'numero idtf')) {
                    return $i;
                }
            }
        }

        return null;
    }

    /**
     * Construit le mapping logique -> index de colonne a partir de la ligne
     * d'en-tete (resiste au reordonnancement via fields[]).
     *
     * @param array<int, mixed> $header
     *
     * @return array<string, int>
     */
    private static function buildColumnMap(array $header): array
    {
        $map = [];

        foreach ($header as $col => $label) {
            $n = self::normalize((string) $label);

            $key = match (true) {
                str_contains($n, 'numero idtf') => 'idtf_number',
                str_contains($n, 'product group'),
                str_contains($n, 'groupe')                => 'product_group',
                str_contains($n, 'nom de la marchandise') => 'name',
                str_contains($n, 'regime de nettoyage')   => 'cleaning_regime',
                str_contains($n, 'exigences importantes') => 'important_requirements',
                str_contains($n, 'date d application')    => 'mandatory_date',
                str_contains($n, 'produits apparentes')   => 'related_products',
                str_contains($n, 'formule')               => 'formula',
                str_contains($n, 'code eural')            => 'eural_code',
                str_contains($n, 'numero cas')            => 'cas',
                str_contains($n, 'annotations')           => 'footnotes',
                default                                   => null,
            };

            if (null !== $key && !isset($map[$key])) {
                $map[$key] = (int) $col;
            }
        }

        return $map;
    }

    /**
     * Convertit une date "dd-mm-yyyy" en "yyyy-mm-dd". Null si format invalide
     * ou date calendaire impossible.
     */
    private static function parseDate(?string $raw): ?string
    {
        if (null === $raw || !preg_match('/^(\d{1,2})-(\d{1,2})-(\d{4})$/', $raw, $m)) {
            return null;
        }

        $day   = (int) $m[1];
        $month = (int) $m[2];
        $year  = (int) $m[3];

        if (!checkdate($month, $day, $year)) {
            return null;
        }

        return sprintf('%04d-%02d-%02d', $year, $month, $day);
    }

    /**
     * Eclate une cellule "Numero CAS" sur ';' en liste de chaines non vides.
     *
     * @return list<string>
     */
    private static function splitCas(?string $raw): array
    {
        if (null === $raw) {
            return [];
        }

        $parts = array_map('trim', explode(';', $raw));

        return array_values(array_filter($parts, static fn (string $v): bool => '' !== $v));
    }

    /**
     * Valeur d'une cellule par index : trim, null si absente/vide.
     *
     * @param array<int, mixed> $row
     */
    private static function val(array $row, ?int $col): ?string
    {
        if (null === $col) {
            return null;
        }

        $v = trim((string) ($row[$col] ?? ''));

        return '' === $v ? null : $v;
    }

    /**
     * Normalise un libelle d'en-tete : minuscules, sans accents ni apostrophes,
     * espaces compresses (pour un matching robuste).
     */
    private static function normalize(string $s): string
    {
        $s = str_replace(['’', "'"], ' ', $s);
        $s = (string) iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $s);
        $s = mb_strtolower($s);

        return trim((string) preg_replace('/\s+/', ' ', $s));
    }
}
