<?php

namespace App\Service;

use App\Entity\ScanSession;

/**
 * Compare les véhicules de deux inventaires.
 * Identité d'un véhicule : VIN normalisé en priorité, sinon plaque normalisée.
 * Les valeurs illisibles/placeholder (UNREADABLE, ERREUR, vide…) ne sont PAS des identifiants :
 * on retombe alors sur la plaque, et un véhicule sans aucun identifiant reste compté (jamais fusionné).
 */
final class InventoryComparator
{
    /** Valeurs OCR ne constituant pas un identifiant exploitable (comparées en alphanumérique majuscule). */
    private const PLACEHOLDERS = ['', 'UNREADABLE', 'ILLISIBLE', 'ERREUR', 'ERROR', 'NA', 'NONLU', 'INCONNU', 'NULL'];

    /**
     * @return array{
     *     added: array<int, array<string, mixed>>,
     *     removed: array<int, array<string, mixed>>,
     *     unchanged: array<int, array<string, mixed>>
     * } added = présents dans B mais pas A ; removed = présents dans A mais pas B ; unchanged = communs.
     */
    public function compare(ScanSession $a, ScanSession $b): array
    {
        $mapA = $this->index($a->getResults(), 'a');
        $mapB = $this->index($b->getResults(), 'b');

        $added = [];
        $unchanged = [];
        foreach ($mapB as $key => $row) {
            if (\array_key_exists($key, $mapA)) {
                $unchanged[] = $row;
            } else {
                $added[] = $row;
            }
        }
        $removed = [];
        foreach ($mapA as $key => $row) {
            if (!\array_key_exists($key, $mapB)) {
                $removed[] = $row;
            }
        }

        return ['added' => $added, 'removed' => $removed, 'unchanged' => $unchanged];
    }

    /**
     * Confronte un inventaire scanné à une liste de stock attendue (importée d'un fichier).
     *
     * @param array<int, array{vin: string, plate: string, designation?: string}> $stock
     *
     * @return array{
     *     missing: array<int, array<string, mixed>>,
     *     extra: array<int, array<string, mixed>>,
     *     matched: array<int, array<string, mixed>>
     * } missing = attendus au stock mais non scannés ; extra = scannés mais absents du stock ; matched = présents des deux côtés.
     */
    public function compareToStock(ScanSession $scan, array $stock): array
    {
        $mapScan = $this->index($scan->getResults(), 'scan');
        $mapStock = $this->index($stock, 'stock');

        $missing = [];
        $matched = [];
        foreach ($mapStock as $key => $row) {
            if (\array_key_exists($key, $mapScan)) {
                $matched[] = $row;
            } else {
                $missing[] = $row;
            }
        }
        $extra = [];
        foreach ($mapScan as $key => $row) {
            if (!\array_key_exists($key, $mapStock)) {
                $extra[] = $row;
            }
        }

        return ['missing' => $missing, 'extra' => $extra, 'matched' => $matched];
    }

    /**
     * Indexe les véhicules par clé d'identité. Les véhicules sans identifiant reçoivent une clé
     * unique (préfixée par le côté A/B) pour ne jamais être fusionnés ni perdus.
     *
     * @param array<int, array<string, mixed>> $results
     *
     * @return array<string, array<string, mixed>>
     */
    private function index(array $results, string $side): array
    {
        $out = [];
        $noId = 0;
        foreach ($results as $r) {
            if (!\is_array($r)) {
                continue;
            }
            $key = $this->key($r);
            if (null === $key) {
                $key = 'noid:' . $side . ':' . $noId++;
            }
            $out[$key] = $r; // doublon de VIN/plaque réel = même véhicule → on garde la dernière occurrence
        }

        return $out;
    }

    /**
     * @param array<string, mixed> $r
     */
    public function key(array $r): ?string
    {
        $vin = $this->identifier($r['vin'] ?? $r['rawVin'] ?? null);
        if (null !== $vin) {
            return 'vin:' . $vin;
        }
        $plate = $this->identifier($r['plate'] ?? $r['rawPlate'] ?? null);
        if (null !== $plate) {
            return 'plate:' . $plate;
        }

        return null;
    }

    /**
     * Normalise une valeur (alphanumérique majuscule) et renvoie null si vide ou placeholder OCR.
     */
    private function identifier(mixed $v): ?string
    {
        if (!\is_string($v)) {
            return null;
        }
        $clean = strtoupper((string) preg_replace('/[^A-Za-z0-9]/', '', $v));
        if ('' === $clean || \in_array($clean, self::PLACEHOLDERS, true)) {
            return null;
        }

        return $clean;
    }

    /**
     * @param array<string, mixed> $r
     */
    public function plate(array $r): string
    {
        $p = $r['plate'] ?? $r['rawPlate'] ?? null;

        return \is_string($p) && '' !== trim($p) ? $p : '-';
    }

    /**
     * @param array<string, mixed> $r
     */
    public function vin(array $r): string
    {
        $v = $r['vin'] ?? $r['rawVin'] ?? null;

        return \is_string($v) && '' !== trim($v) ? $v : '-';
    }
}
