"""Évaluation multilabel locale. Python 3.10+, bibliothèque standard uniquement.

Les exemples et prédictions livrés sont fictifs. Aucun modèle n'est appelé.
Les métriques P/R/F1 portent seulement sur les documents au statut ok.
Une ligne absente compte comme missing dans le dénominateur total.
"""
import argparse
import csv
import json
from collections import Counter
from pathlib import Path


def load_csv(path, required):
    with Path(path).open(encoding="utf-8-sig", newline="") as f:
        reader = csv.DictReader(f)
        if not required.issubset(set(reader.fieldnames or [])):
            raise ValueError(f"{path}: colonnes requises: {sorted(required)}")
        rows = {}
        for row in reader:
            if None in row or any(v is None for v in row.values()):
                raise ValueError(f"{path}: ligne CSV mal formée")
            ident = row["id"].strip()
            if not ident or ident in rows:
                raise ValueError(f"{path}: identifiant vide ou dupliqué: {ident!r}")
            rows[ident] = row
        return rows


def labels_of(value, allowed):
    parts = [] if not value.strip() else [x.strip() for x in value.split("|")]
    labels = set(parts)
    if len(parts) != len(labels) or not labels.issubset(allowed):
        raise ValueError(f"Étiquettes inconnues ou dupliquées: {value!r}")
    return labels


def ratio(a, b):
    return a / b if b else None


def metrics(tp, fp, fn):
    return {"tp": tp, "fp": fp, "fn": fn,
            "precision": ratio(tp, tp + fp),
            "recall": ratio(tp, tp + fn),
            "f1": ratio(2 * tp, 2 * tp + fp + fn)}


def evaluate(reference, predictions, allowed):
    if not reference:
        raise ValueError("La référence est vide")
    extras = set(predictions) - set(reference)
    if extras:
        raise ValueError(f"Prédictions hors corpus: {sorted(extras)}")
    statuses = Counter({s: 0 for s in ("ok", "abstain", "error", "missing")})
    counts = {label: Counter(tp=0, fp=0, fn=0, support_total=0) for label in allowed}
    correct = 0
    errors = []
    for ident, row in reference.items():
        truth = labels_of(row["labels"], allowed)
        for label in truth:
            counts[label]["support_total"] += 1
        pred = predictions.get(ident)
        if pred is None:
            statuses["missing"] += 1
            errors.append({"id": ident, "status": "missing"})
            continue
        status = pred["status"].strip()
        if status not in {"ok", "abstain", "error"}:
            raise ValueError(f"{ident}: statut inconnu {status!r}")
        guessed = labels_of(pred["labels"], allowed)
        if status != "ok" and guessed:
            raise ValueError(f"{ident}: une abstention/erreur ne doit pas porter d'étiquettes")
        statuses[status] += 1
        if status != "ok":
            errors.append({"id": ident, "status": status})
            continue
        correct += guessed == truth
        for label in allowed:
            counts[label]["tp"] += label in truth and label in guessed
            counts[label]["fp"] += label not in truth and label in guessed
            counts[label]["fn"] += label in truth and label not in guessed
        if guessed != truth:
            errors.append({"id": ident, "status": "ok", "missing_labels": sorted(truth - guessed),
                           "extra_labels": sorted(guessed - truth)})
    by_label = {}
    for label in sorted(allowed):
        co = counts[label]
        by_label[label] = {**metrics(co["tp"], co["fp"], co["fn"]),
                           "support_auto": co["tp"] + co["fn"], "support_total": co["support_total"]}
    totals = [sum(co[key] for co in counts.values()) for key in ("tp", "fp", "fn")]
    return {"documents_total": len(reference), "statuses": dict(statuses),
            "coverage_auto": ratio(statuses["ok"], len(reference)),
            "exact_auto": ratio(correct, statuses["ok"]),
            "exact_auto_over_total": ratio(correct, len(reference)),
            "exact_auto_count": correct,
            "metric_scope": "P/R/F1 calculés uniquement sur les documents ok; lire avec la couverture.",
            "micro_auto": metrics(*totals), "by_label_auto": by_label,
            "errors_or_unhandled": errors}


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("reference")
    parser.add_argument("predictions")
    parser.add_argument("--taxonomy", default="taxonomie.json")
    args = parser.parse_args()
    try:
        taxonomy = json.loads(Path(args.taxonomy).read_text(encoding="utf-8"))
        definitions = taxonomy["labels"]
        if not isinstance(definitions, dict) or not definitions or any(not k for k in definitions):
            raise ValueError("Taxonomie vide ou invalide")
        reference = load_csv(args.reference, {"id", "labels"})
        predictions = load_csv(args.predictions, {"id", "status", "labels"})
        result = evaluate(reference, predictions, set(definitions))
    except (ValueError, KeyError, OSError) as exc:
        parser.exit(2, f"Erreur: {exc}\n")
    print(json.dumps(result, ensure_ascii=False, indent=2, allow_nan=False))


if __name__ == "__main__":
    main()
