<?php

namespace App\Service;

/**
 * Détermine si la plaque ou le VIN d'un véhicule scanné est « en erreur » (à vérifier/corriger).
 * Source de vérité unique, réutilisée par la page détail (surbrillance), les exports XLSX/PDF
 * et toute alerte. Mêmes règles que les badges de l'écran de fin de scan (scan_controller.js).
 */
final class VehicleErrorChecker
{
    private const PLATE_SIV = '/^[A-Z]{2}\d{3}[A-Z]{2}$/';
    private const PLATE_OLD = '/^\d{1,4}[A-Z]{1,3}\d{2,3}$/';
    private const VIN_VALID = '/^[A-HJ-NPR-Z0-9]{17}$/i';

    /**
     * La plaque est en erreur si une valeur est présente mais douteuse (format invalide ou
     * faible confiance), ou s'il ne reste qu'une lecture brute non validée. Une plaque
     * absente n'est PAS une erreur (la plaque est facultative ; seul le VIN est obligatoire).
     *
     * @param array<string, mixed> $r
     */
    public function plateInError(array $r): bool
    {
        $plate = $r['plate'] ?? null;
        if (\is_string($plate) && '' !== $plate) {
            return !self::isPlateValid($plate) || 'low' === ($r['plateConfidence'] ?? null);
        }

        return !empty($r['rawPlate']);
    }

    /**
     * Le VIN est obligatoire : en erreur s'il manque (pas d'image), s'il est illisible
     * (image présente mais aucune valeur), ou douteux (format invalide / faible confiance).
     *
     * @param array<string, mixed> $r
     */
    public function vinInError(array $r): bool
    {
        if (empty($r['vinImageId'])) {
            return true;
        }
        $vin = $r['vin'] ?? null;
        if (\is_string($vin) && '' !== $vin) {
            return !self::isVinValid($vin) || 'low' === ($r['vinConfidence'] ?? null);
        }

        return true;
    }

    /**
     * Vrai si au moins un véhicule du scan est en erreur (plaque ou VIN).
     * Alimente le filtre « Anomalies » de la liste des inventaires.
     *
     * @param array<int, array<string, mixed>> $results
     */
    public function sessionHasErrors(array $results): bool
    {
        foreach ($results as $r) {
            if (\is_array($r) && ($this->plateInError($r) || $this->vinInError($r))) {
                return true;
            }
        }

        return false;
    }

    /**
     * Positions (0-based) des véhicules en doublon dans un même scan : deux véhicules
     * partageant la même plaque OU le même VIN (valeur normalisée non vide). La valeur
     * validée prime, à défaut la lecture brute.
     *
     * @param array<int, array<string, mixed>> $results
     *
     * @return list<int>
     */
    public function duplicateIndexes(array $results): array
    {
        $byVin = [];
        $byPlate = [];
        foreach (array_values($results) as $i => $r) {
            if (!\is_array($r)) {
                continue;
            }
            $vin = self::normalize((string) ($r['vin'] ?? $r['rawVin'] ?? ''));
            $plate = self::normalize((string) ($r['plate'] ?? $r['rawPlate'] ?? ''));
            if ('' !== $vin) {
                $byVin[$vin][] = $i;
            }
            if ('' !== $plate) {
                $byPlate[$plate][] = $i;
            }
        }

        $dups = [];
        foreach ([$byVin, $byPlate] as $map) {
            foreach ($map as $indexes) {
                if (\count($indexes) > 1) {
                    foreach ($indexes as $idx) {
                        $dups[$idx] = true;
                    }
                }
            }
        }

        $out = array_keys($dups);
        sort($out);

        return $out;
    }

    /**
     * Positions (0-based) des véhicules à SUPPRIMER pour dédupliquer : pour chaque groupe
     * de doublons (même plaque ou même VIN), on conserve la PREMIÈRE occurrence et on
     * renvoie les suivantes. Complémentaire de duplicateIndexes() (qui marque tout le groupe).
     *
     * @param array<int, array<string, mixed>> $results
     *
     * @return list<int>
     */
    public function redundantIndexes(array $results): array
    {
        $seenVin = [];
        $seenPlate = [];
        $remove = [];
        foreach (array_values($results) as $i => $r) {
            if (!\is_array($r)) {
                continue;
            }
            $vin = self::normalize((string) ($r['vin'] ?? $r['rawVin'] ?? ''));
            $plate = self::normalize((string) ($r['plate'] ?? $r['rawPlate'] ?? ''));
            $isDup = ('' !== $vin && isset($seenVin[$vin])) || ('' !== $plate && isset($seenPlate[$plate]));
            if ($isDup) {
                $remove[] = $i;
                continue;
            }
            if ('' !== $vin) {
                $seenVin[$vin] = true;
            }
            if ('' !== $plate) {
                $seenPlate[$plate] = true;
            }
        }

        return $remove;
    }

    public static function normalize(string $v): string
    {
        return strtoupper(preg_replace('/[\s-]/', '', $v) ?? '');
    }

    public static function isPlateValid(string $v): bool
    {
        $s = strtoupper(preg_replace('/[\s-]/', '', $v) ?? '');

        return 1 === preg_match(self::PLATE_SIV, $s) || 1 === preg_match(self::PLATE_OLD, $s);
    }

    public static function isVinValid(string $v): bool
    {
        return 1 === preg_match(self::VIN_VALID, preg_replace('/[\s-]/', '', $v) ?? '');
    }
}
