<?php

namespace App\Service;

use OpenSpout\Reader\XLSX\Reader as XlsxReader;

/**
 * Lit la « photo du stock » (.xlsx, export DMS) en une liste de véhicules attendus.
 * CSV volontairement non supporté : les CSV d'Excel français (séparateur « ; »)
 * étaient lus comme une seule colonne (le lecteur attend des virgules).
 *
 * Format attendu : un véhicule par ligne, colonnes
 * ID_CONCESSION · DATE_STOCK · TYPE · IMMATRICULATION · VIN · LOCALISATION.
 * La concession est référencée par son ID (référentiel importé dans Paramètres),
 * jamais par son nom. Détection des colonnes par leur en-tête (cherché dans les
 * premières lignes, insensible à la casse/accents/underscores) ; seule la colonne
 * VIN est indispensable à la détection.
 *
 * Les lignes de titre précédant l'en-tête sont ignorées. À défaut d'en-tête reconnu,
 * repli positionnel dans l'ordre du format : A = ID_CONCESSION, B = DATE_STOCK,
 * C = TYPE, D = IMMATRICULATION, E = VIN, F = LOCALISATION.
 */
final class StockImporter
{
    private const CONCESSION_ID_HEADERS = ['idconcession', 'concessionid', 'codeconcession', 'idconc', 'concession', 'concessions'];
    private const DATE_HEADERS = ['datestock', 'datedustock', 'datedestocks', 'date'];
    private const TYPE_HEADERS = ['type', 'typevehicule', 'typedevehicule'];
    private const PLATE_HEADERS = ['immatriculation', 'immat', 'plaque', 'immatriculations'];
    private const VIN_HEADERS = ['vin', 'chassis', 'nchassis', 'numerochassis', 'numdechassis', 'numserie', 'numerodeserie'];
    private const LOCALISATION_HEADERS = ['localisation', 'localisations', 'emplacement', 'parc'];

    /** Nombre de lignes en tête de fichier dans lesquelles chercher la ligne d'en-tête. */
    private const HEADER_SEARCH_ROWS = 10;

    /**
     * @return array{
     *     rows: list<array{dateStock: ?\DateTimeImmutable, concessionId: string, type: string, plate: string, vin: string, localisation: string}>,
     *     headerDetected: bool
     * }
     *
     * @throws \RuntimeException format non supporté, fichier vide ou aucune colonne exploitable
     *
     * @param callable(int): void|null $onProgress appelé périodiquement avec le nombre de véhicules lus
     */
    public function import(string $path, string $extension, ?callable $onProgress = null): array
    {
        $reader = match (strtolower($extension)) {
            'xlsx' => new XlsxReader(),
            default => throw new \RuntimeException('Format non supporté. Utilisez un fichier Excel (.xlsx).'),
        };
        $reader->open($path);

        $rows = [];
        $headerDetected = false;
        // Repli positionnel par défaut, dans l'ordre du format DMS.
        $map = ['concessionId' => 0, 'dateStock' => 1, 'type' => 2, 'plate' => 3, 'vin' => 4, 'localisation' => 5];

        // Lignes lues avant d'avoir tranché en-tête / repli positionnel : si un en-tête est
        // trouvé plus bas, ce sont des lignes de titre à jeter ; sinon ce sont des données.
        $pending = [];
        $searching = true;

        try {
            foreach ($reader->getSheetIterator() as $sheet) {
                $rowIndex = 0;
                foreach ($sheet->getRowIterator() as $row) {
                    ++$rowIndex;
                    $cells = $row->toArray();

                    if ($searching) {
                        $detected = $this->detectColumns($cells);
                        if (null !== $detected) {
                            $map = $detected;
                            $headerDetected = true;
                            $searching = false;
                            $pending = []; // lignes de titre avant l'en-tête → ignorées
                            continue;
                        }
                        $pending[] = $cells;
                        if ($rowIndex >= self::HEADER_SEARCH_ROWS) {
                            // Pas d'en-tête reconnu : tout est donnée (repli positionnel).
                            $searching = false;
                            foreach ($pending as $p) {
                                $this->appendRow($rows, $p, $map);
                            }
                            $pending = [];
                        }
                        continue;
                    }

                    $this->appendRow($rows, $cells, $map);
                    if (null !== $onProgress && 0 === $rowIndex % 250) {
                        $onProgress(\count($rows));
                    }
                }

                break; // première feuille uniquement
            }
        } finally {
            $reader->close();
        }

        // Fichier court (≤ lignes de recherche) sans en-tête : les lignes bufferisées sont des données.
        foreach ($pending as $p) {
            $this->appendRow($rows, $p, $map);
        }

        if ([] === $rows) {
            throw new \RuntimeException('Aucun véhicule trouvé. Le fichier doit contenir les colonnes ID_CONCESSION, DATE_STOCK, TYPE, IMMATRICULATION, VIN, LOCALISATION.');
        }

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

    /**
     * Ajoute une ligne de données si elle porte au moins un identifiant (VIN ou plaque).
     *
     * @param list<array{dateStock: ?\DateTimeImmutable, concessionId: string, type: string, plate: string, vin: string, localisation: string}> $rows
     * @param array<int, mixed>                                                                                                                 $cells
     * @param array{concessionId: ?int, dateStock: ?int, type: ?int, plate: ?int, vin: ?int, localisation: ?int}                                $map
     */
    private function appendRow(array &$rows, array $cells, array $map): void
    {
        $vin = $this->cell($cells, $map['vin']);
        $plate = $this->cell($cells, $map['plate']);

        if ('' === $vin && '' === $plate) {
            return; // ligne sans identifiant → ignorée
        }
        $rows[] = [
            'dateStock' => $this->parseDate(null !== $map['dateStock'] ? ($cells[$map['dateStock']] ?? null) : null),
            'concessionId' => $this->cell($cells, $map['concessionId']),
            'type' => $this->cell($cells, $map['type']),
            'plate' => $plate,
            'vin' => $vin,
            'localisation' => $this->cell($cells, $map['localisation']),
        ];
    }

    /**
     * @param array<int, mixed> $headerCells
     *
     * @return array{concessionId: ?int, dateStock: ?int, type: ?int, plate: ?int, vin: ?int, localisation: ?int}|null null si aucun en-tête VIN reconnu
     */
    private function detectColumns(array $headerCells): ?array
    {
        $found = ['concessionId' => null, 'dateStock' => null, 'type' => null, 'plate' => null, 'vin' => null, 'localisation' => null];
        $aliases = [
            'concessionId' => self::CONCESSION_ID_HEADERS,
            'dateStock' => self::DATE_HEADERS,
            'type' => self::TYPE_HEADERS,
            'plate' => self::PLATE_HEADERS,
            'vin' => self::VIN_HEADERS,
            'localisation' => self::LOCALISATION_HEADERS,
        ];

        foreach ($headerCells as $i => $raw) {
            if (!\is_string($raw) && !is_numeric($raw)) {
                continue;
            }
            $h = self::normalize((string) $raw);
            if ('' === $h) {
                continue;
            }
            foreach ($aliases as $field => $headers) {
                if (null === $found[$field] && \in_array($h, $headers, true)) {
                    $found[$field] = $i;
                    break;
                }
            }
        }

        if (null === $found['vin']) {
            return null; // pas d'en-tête exploitable (le VIN identifie la ligne d'en-tête)
        }

        return $found;
    }

    /**
     * Valeur de date d'une cellule : objet date OpenSpout, numéro de série Excel ou chaîne.
     */
    private function parseDate(mixed $v): ?\DateTimeImmutable
    {
        if ($v instanceof \DateTimeImmutable) {
            return $v;
        }
        if ($v instanceof \DateTimeInterface) {
            return \DateTimeImmutable::createFromInterface($v);
        }
        if (\is_int($v) || \is_float($v)) {
            // Numéro de série Excel (jours depuis 1899-12-30), plage plausible 1954-2118.
            if ($v >= 20000 && $v < 80000) {
                return (new \DateTimeImmutable('1899-12-30'))->modify(sprintf('+%d days', (int) $v));
            }

            return null;
        }
        if (!\is_string($v)) {
            return null;
        }
        $v = trim($v);
        if ('' === $v) {
            return null;
        }
        foreach (['d/m/Y', 'd/m/y', 'Y-m-d', 'd-m-Y', 'd.m.Y'] as $format) {
            $d = \DateTimeImmutable::createFromFormat('!' . $format, $v);
            if (false !== $d) {
                return $d;
            }
        }

        return null;
    }

    /**
     * @param array<int, mixed> $cells
     */
    private function cell(array $cells, ?int $index): string
    {
        if (null === $index || !isset($cells[$index])) {
            return '';
        }
        $value = $cells[$index];
        if ($value instanceof \DateTimeInterface) {
            return $value->format('d/m/Y');
        }
        // IDs numériques : Excel renvoie des int/float → "123" et pas "123.0"
        if (\is_int($value)) {
            return (string) $value;
        }
        if (\is_float($value) && $value == (int) $value) {
            return (string) (int) $value;
        }

        return trim((string) $value);
    }

    /**
     * Classe un libellé de TYPE en 'VN' (neuf) ou 'VO' (occasion), ou null si indéterminé.
     * Tolère VN/VO, NEUF/OCCASION (et variantes accentuées/espacées via normalize()).
     */
    public static function classifyVnVo(string $type): ?string
    {
        $n = self::normalize($type);
        if ('' === $n) {
            return null;
        }
        if ('vn' === $n || str_contains($n, 'neuf')) {
            return 'VN';
        }
        if ('vo' === $n || str_contains($n, 'occasion')) {
            return 'VO';
        }
        // Préfixe (ex. "VN ESSENCE" → "vnessence")
        if (str_starts_with($n, 'vn')) {
            return 'VN';
        }
        if (str_starts_with($n, 'vo')) {
            return 'VO';
        }

        return null;
    }

    /** Minuscule sans accents ni séparateurs, pour comparer en-têtes, IDs et noms de façon tolérante. */
    public static function normalize(string $v): string
    {
        $v = mb_strtolower(trim($v));
        $v = strtr($v, ['à' => 'a', 'â' => 'a', 'ä' => 'a', 'é' => 'e', 'è' => 'e', 'ê' => 'e', 'ë' => 'e', 'î' => 'i', 'ï' => 'i', 'ô' => 'o', 'ö' => 'o', 'ù' => 'u', 'û' => 'u', 'ü' => 'u', 'ç' => 'c']);

        return (string) preg_replace('/[^a-z0-9]/', '', $v);
    }
}
