#!/usr/bin/env python3
"""Compare deux modèles simples sans ouvrir le test pendant le choix."""

import csv
import random
import sys
from pathlib import Path

FICHIER = Path(__file__).with_name("fret_comparaison.csv")
GRAINE = 404
VOISINS = 3


def charger(fichier=FICHIER):
    with fichier.open(encoding="utf-8", newline="") as flux:
        lignes = list(csv.DictReader(flux))
    attendues = {"order_id", "order_item_id", "poids_g", "volume_cm3", "fret_eleve"}
    if not lignes or not attendues.issubset(lignes[0]):
        raise ValueError(f"Colonnes attendues : {sorted(attendues)}")
    resultat = []
    for rang, ligne in enumerate(lignes):
        order_id = (ligne.get("order_id") or "").strip()
        item_brut = (ligne.get("order_item_id") or "").strip()
        if not order_id or not item_brut:
            raise ValueError("Clé article vide")
        order_item_id = int(item_brut)
        if order_item_id <= 0:
            raise ValueError("order_item_id doit être positif")
        resultat.append({
            "id": (order_id, order_item_id),
            "rang": rang,
            "poids_g": float(ligne["poids_g"]),
            "fret_eleve": int(ligne["fret_eleve"]),
        })
    ids = [ligne["id"] for ligne in resultat]
    if len(ids) != len(set(ids)):
        raise ValueError("Clé article dupliquée")
    return resultat


def separer(lignes):
    rng = random.Random(GRAINE)
    groupes = {c: [x for x in lignes if x["fret_eleve"] == c] for c in (0, 1)}
    if any(len(groupe) <= 16 for groupe in groupes.values()):
        raise ValueError(
            "Il faut au moins 17 lignes de chaque classe pour réserver "
            "8 lignes au test, 8 à la validation et en garder à l'entraînement"
        )
    for groupe in groupes.values():
        rng.shuffle(groupe)
    test = groupes[0][:8] + groupes[1][:8]
    validation = groupes[0][8:16] + groupes[1][8:16]
    entrainement = groupes[0][16:] + groupes[1][16:]
    if len(entrainement) < VOISINS:
        raise ValueError(
            f"Il faut au moins {VOISINS} lignes d'entraînement "
            "pour comparer ce nombre de voisins"
        )
    lots = (entrainement, validation, test)
    ids = [{x["id"] for x in lot} for lot in lots]
    source_ids = {x["id"] for x in lignes}
    if ids[0] & ids[1] or ids[0] & ids[2] or ids[1] & ids[2] or set().union(*ids) != source_ids:
        raise ValueError("Partition incohérente des clés article")
    return lots


def predire_arbre(modele, ligne):
    sous_seuil = ligne["poids_g"] <= modele["seuil"]
    return modele["classe_sous_seuil"] if sous_seuil else 1 - modele["classe_sous_seuil"]


def entrainer_arbre(lignes):
    valeurs = sorted({x["poids_g"] for x in lignes})
    if len(valeurs) < 2:
        raise ValueError("Il faut au moins deux poids différents pour apprendre une coupure")
    meilleur = None
    for seuil in ((a + b) / 2 for a, b in zip(valeurs, valeurs[1:])):
        for classe in (0, 1):
            modele = {"seuil": seuil, "classe_sous_seuil": classe}
            bonnes = sum(predire_arbre(modele, x) == x["fret_eleve"] for x in lignes)
            cle = (bonnes, -seuil, -classe)
            if meilleur is None or cle > meilleur[0]:
                meilleur = (cle, modele)
    return meilleur[1]


def predire_voisins(entrainement, ligne):
    proches = sorted(
        entrainement,
        key=lambda x: (abs(x["poids_g"] - ligne["poids_g"]), x["rang"]),
    )[:VOISINS]
    return int(sum(x["fret_eleve"] for x in proches) > VOISINS / 2)


def justesse(predire, lignes):
    return sum(predire(x) == x["fret_eleve"] for x in lignes) / len(lignes)


def preparer():
    entrainement, validation, test = separer(charger())
    arbre = entrainer_arbre(entrainement)
    predictions = {
        "arbre": lambda x: predire_arbre(arbre, x),
        "voisins": lambda x: predire_voisins(entrainement, x),
    }
    scores_validation = {nom: justesse(predire, validation) for nom, predire in predictions.items()}
    # Règle fixée avant le test : l'arbre est gardé en cas d'égalité.
    retenu = "arbre" if scores_validation["arbre"] >= scores_validation["voisins"] else "voisins"
    return entrainement, validation, test, arbre, predictions, scores_validation, retenu


def main():
    if len(sys.argv) != 2 or sys.argv[1] not in {"observer", "comparer", "tester"}:
        print("Usage : py comparer_modeles.py observer|comparer|tester")
        return 2
    entrainement, validation, test, arbre, predictions, scores, retenu = preparer()
    ids = [{x["id"] for x in lot} for lot in (entrainement, validation, test)]
    print(f"Entraînement : {len(entrainement)}")
    print(f"Validation : {len(validation)}")
    print(f"Test réservé : {len(test)}")
    print(f"Chevauchements : {len(ids[0]&ids[1])}, {len(ids[0]&ids[2])}, {len(ids[1]&ids[2])}")
    print(f"Union des clés article : {len(set().union(*ids))}")
    if sys.argv[1] == "observer":
        print("Cibles :", {c: sum(x["fret_eleve"] == c for x in charger()) for c in (0, 1)})
        return 0
    print(f"Arbre appris : poids <= {arbre['seuil']:.1f} prédit {arbre['classe_sous_seuil']}")
    print(f"Validation arbre : {scores['arbre']:.3f}")
    print(f"Validation voisins (k={VOISINS}) : {scores['voisins']:.3f}")
    print(f"Modèle retenu : {retenu}")
    if sys.argv[1] == "comparer":
        print("TEST : NON CONSULTÉ")
        return 0
    print(f"Justesse finale sur test : {justesse(predictions[retenu], test):.3f}")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
