#!/usr/bin/env python3
"""Entraîne un arbre à une coupure sans bibliothèque externe."""

import csv
import random
import sys
from pathlib import Path

FICHIER = Path(__file__).with_name("fret_olist.csv")
COLONNES_ENTREE = ["poids_g"]
COLONNE_CIBLE = "fret_eleve"
TEST_PAR_CLASSE = 8
GRAINE = 404


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", COLONNE_CIBLE}
    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")
        cible = int(ligne[COLONNE_CIBLE])
        if cible not in (0, 1):
            raise ValueError(f"Enregistrement {rang + 1} : {COLONNE_CIBLE} doit valoir 0 ou 1")
        resultat.append(
            {"id": (order_id, order_item_id), "rang": rang}
            | {nom: float(ligne[nom]) for nom in ("poids_g", "volume_cm3")}
            | {COLONNE_CIBLE: cible}
        )
    ids = [ligne["id"] for ligne in resultat]
    if len(ids) != len(set(ids)):
        raise ValueError("Clé article dupliquée")
    return resultat


def observer(lignes):
    print("Colonnes : poids_g, volume_cm3, fret_eleve")
    print("Premières lignes :")
    for ligne in lignes[:5]:
        print({nom: ligne[nom] for nom in ("poids_g", "volume_cm3", COLONNE_CIBLE)})
    print("Effectifs par cible :", {c: sum(x[COLONNE_CIBLE] == c for x in lignes) for c in (0, 1)})


def separer(lignes):
    rng = random.Random(GRAINE)
    groupes = {c: [i for i, x in enumerate(lignes) if x[COLONNE_CIBLE] == c] for c in (0, 1)}
    if any(len(indices) <= TEST_PAR_CLASSE for indices in groupes.values()):
        raise ValueError(
            f"Il faut au moins {TEST_PAR_CLASSE + 1} lignes de chaque classe "
            f"pour en réserver {TEST_PAR_CLASSE} au test et en garder à l'entraînement"
        )
    for indices in groupes.values():
        rng.shuffle(indices)
    test_rangs = set(groupes[0][:TEST_PAR_CLASSE] + groupes[1][:TEST_PAR_CLASSE])
    entrainement_rangs = set(range(len(lignes))) - test_rangs
    entrainement = [x for i, x in enumerate(lignes) if i in entrainement_rangs]
    test = [x for i, x in enumerate(lignes) if i in test_rangs]
    entrainement_ids = {x["id"] for x in entrainement}
    test_ids = {x["id"] for x in test}
    source_ids = {x["id"] for x in lignes}
    if entrainement_ids & test_ids or entrainement_ids | test_ids != source_ids:
        raise ValueError("Partition incohérente des clés article")
    return entrainement, test, entrainement_ids, test_ids


def predire(modele, ligne):
    comparaison = ligne[modele["colonne"]] <= modele["seuil"]
    return modele["si_inferieur"] if comparaison else 1 - modele["si_inferieur"]


def entrainer(lignes):
    meilleur = None
    for colonne in COLONNES_ENTREE:
        valeurs = sorted({x[colonne] for x in lignes})
        seuils = [(a + b) / 2 for a, b in zip(valeurs, valeurs[1:])]
        for seuil in seuils:
            for si_inferieur in (0, 1):
                candidat = {"colonne": colonne, "seuil": seuil, "si_inferieur": si_inferieur}
                bonnes = sum(predire(candidat, x) == x[COLONNE_CIBLE] for x in lignes)
                cle = (bonnes, -seuil, colonne, -si_inferieur)
                if meilleur is None or cle > meilleur[0]:
                    meilleur = (cle, candidat)
    if meilleur is None:
        raise ValueError("Pas assez de valeurs différentes pour entraîner le modèle")
    return meilleur[1]


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


def main():
    lignes = charger()
    if len(sys.argv) == 2 and sys.argv[1] == "observer":
        observer(lignes)
        return
    entrainement, test, entrainement_ids, test_ids = separer(lignes)
    modele = entrainer(entrainement)
    print(f"Entraînement : {len(entrainement)} lignes")
    print(f"Test : {len(test)} lignes")
    print(f"Chevauchement : {len(entrainement_ids & test_ids)} ligne")
    print(f"Union des clés article : {len(entrainement_ids | test_ids)}")
    sens = "<= donne 1" if modele["si_inferieur"] else "> donne 1"
    print(f"Modèle appris : {modele['colonne']}, seuil {modele['seuil']:.1f}, {sens}")
    print(f"Justesse entraînement : {justesse(modele, entrainement):.3f}")
    print(f"Justesse test : {justesse(modele, test):.3f}")
    assert len(entrainement) + len(test) == len(lignes)
    assert not entrainement_ids & test_ids
    assert len(test_ids) == len(test)


if __name__ == "__main__":
    main()
