#!/usr/bin/env python3
"""
Classe les PDF déjà indexés dans extractions_pdf_indexed_all/.

Sorties :
- pdf_collecteur/state/classification.csv
- pdf_collecteur/state/sources_prioritaires.csv

Le classement est heuristique et traçable : chaque ligne contient un score et
des indices. Il sert à piloter la génération pédagogique, pas à remplacer la
validation humaine.
"""

from __future__ import annotations

import argparse
import csv
import re
import sys
import unicodedata
from collections import defaultdict
from pathlib import Path


SCRIPT_DIR = Path(__file__).resolve().parent
PROGRAMME_DIR = SCRIPT_DIR.parent
DEFAULT_INDEX = PROGRAMME_DIR / "extractions_pdf_indexed_all"
DEFAULT_OUT = SCRIPT_DIR / "state"


CLASSIFICATION_FIELDS = [
    "Id",
    "RelativePath",
    "Cycle",
    "Matiere",
    "Niveau",
    "TypeDocument",
    "UsagePrioritaire",
    "Score",
    "Indices",
    "TextFile",
    "InfoFile",
    "SHA256",
]

PRIORITY_FIELDS = [
    "PriorityRank",
    "Cycle",
    "Matiere",
    "Niveau",
    "TypeDocument",
    "UsagePrioritaire",
    "Score",
    "Id",
    "RelativePath",
    "Indices",
]


LEVEL_TERMS = {
    "CM1": ["cm1"],
    "CM2": ["cm2"],
    "6e": ["6e", "6eme", "sixième", "sixieme"],
    "5e": ["5e", "5eme", "cinquième", "cinquieme"],
    "4e": ["4e", "4eme", "quatrième", "quatrieme"],
    "3e": ["3e", "3eme", "troisième", "troisieme"],
}

DOC_TYPE_TERMS = {
    "programme_officiel": [
        "programme d'enseignement",
        "programme de ",
        "annexe",
        "boen",
        "bulletin officiel",
    ],
    "attendus_reperes": [
        "attendus de fin d'année",
        "attendus de fin d’annee",
        "attendus de fin de cycle",
        "repères annuels",
        "reperes annuels",
        "repères de progression",
        "reperes de progression",
    ],
    "guide_enseignant": [
        "guide",
        "vademecum",
        "document maître",
        "doc maitre",
        "document maitre",
        "ressource d'accompagnement",
        "ressources d'accompagnement",
        "mise en œuvre",
        "mise en oeuvre",
        "l'enseignant",
        "enseignant",
    ],
    "activite_sequence": [
        "séquence",
        "sequence",
        "situation d'apprentissage",
        "situation-problème",
        "situation probleme",
        "activité",
        "activite",
        "exemples de mise en œuvre",
        "exemplesmiseenoeuvre",
    ],
    "exercice": [
        "exercice",
        "questions flash",
        "question flash",
        "tâche",
        "tache",
        "calcul mental",
        "compléter",
        "complete",
        "calcule",
        "réponds",
        "reponds",
    ],
    "evaluation": [
        "évaluation",
        "evaluation",
        "évaluer",
        "evaluer",
        "critères de réussite",
        "criteres de reussite",
        "niveau de maîtrise",
        "niveau de maitrise",
        "situations d'évaluation",
        "situationsdevaluation",
    ],
}

USAGE_BY_TYPE = {
    "programme_officiel": "cadrage_programme",
    "attendus_reperes": "progression_niveau",
    "guide_enseignant": "source_pedagogique",
    "activite_sequence": "activite_transformable",
    "exercice": "exercices",
    "evaluation": "diagnostic_evaluation",
}


def fold(value: str) -> str:
    value = unicodedata.normalize("NFKD", value or "")
    value = "".join(ch for ch in value if not unicodedata.combining(ch))
    return value.casefold()


def read_csv(path: Path) -> list[dict[str, str]]:
    with path.open("r", encoding="utf-8-sig", newline="") as handle:
        return list(csv.DictReader(handle))


def write_csv(path: Path, fields: list[str], rows: list[dict[str, str]]) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    with path.open("w", encoding="utf-8-sig", newline="") as handle:
        writer = csv.DictWriter(handle, fieldnames=fields, extrasaction="ignore")
        writer.writeheader()
        writer.writerows(rows)


def infer_cycle(relative_path: str, text: str) -> tuple[str, list[str], int]:
    folded = fold(relative_path + " " + text[:3000])
    if relative_path.startswith("Cycle 3"):
        return "cycle_3", ["path:Cycle 3"], 95
    if relative_path.startswith("Cycle 4"):
        return "cycle_4", ["path:Cycle 4"], 95
    if "cycle 3" in folded or "cycle_3" in folded:
        return "cycle_3", ["text:cycle 3"], 70
    if "cycle 4" in folded or "cycle_4" in folded:
        return "cycle_4", ["text:cycle 4"], 70
    return "inconnu", ["cycle:non_detecte"], 10


def infer_subject(relative_path: str) -> tuple[str, list[str], int]:
    parts = relative_path.split("\\")
    if len(parts) >= 3 and parts[0] in {"Cycle 3", "Cycle 4"}:
        return normalize_subject(parts[1]), [f"path:{parts[1]}"], 95
    folded = fold(relative_path)
    subject_terms = {
        "mathematiques": ["math", "mathematiques", "mathématiques"],
        "francais": ["francais", "français"],
        "histoire_geographie": ["histoire", "geographie", "géographie"],
        "histoire_des_arts": ["histoire des arts", "hart"],
        "emc": ["enseignement moral", "civique", "emc"],
        "physique_chimie": ["physique", "chimie", "phch"],
        "svt": ["svt", "sciences de la vie"],
        "eps": ["eps", "education physique", "éducation physique"],
        "langues": ["anglais", "allemand", "langues"],
    }
    for subject, terms in subject_terms.items():
        if any(fold(term) in folded for term in terms):
            return subject, [f"path_term:{subject}"], 65
    return "inconnu", ["matiere:non_detectee"], 10


def normalize_subject(value: str) -> str:
    mapping = {
        "Math": "mathematiques",
        "Français": "francais",
        "Histoire-Geographie": "histoire_geographie",
        "Histoire des arts": "histoire_des_arts",
        "Moral&Civisme": "emc",
        "Physique-Chimie": "physique_chimie",
        "SVT": "svt",
        "EPS": "eps",
        "Langues": "langues",
        "Relationnel": "relationnel",
        "Autre": "autre",
    }
    return mapping.get(value, fold(value).replace(" ", "_").replace("&", "_"))


def infer_level(relative_path: str, text: str, cycle: str) -> tuple[str, list[str], int]:
    primary = fold(relative_path)
    secondary = fold(text[:6000])
    primary_scores: dict[str, int] = {}
    secondary_scores: dict[str, int] = {}
    indices: list[str] = []
    for level, terms in LEVEL_TERMS.items():
        for term in terms:
            pattern = rf"(^|[^a-z0-9]){re.escape(fold(term))}([^a-z0-9]|$)"
            if fold(term) in primary:
                primary_scores[level] = primary_scores.get(level, 0) + 1
            if re.search(pattern, secondary):
                secondary_scores[level] = secondary_scores.get(level, 0) + 1
    if primary_scores:
        level, score = sorted(primary_scores.items(), key=lambda item: item[1], reverse=True)[0]
        indices.append(f"niveau_path:{level}")
        return level, indices, min(55 + score * 15, 95)
    scores = secondary_scores
    if scores:
        level, score = sorted(scores.items(), key=lambda item: item[1], reverse=True)[0]
        indices.append(f"niveau:{level}")
        return level, indices, min(40 + score * 15, 90)
    if cycle == "cycle_3":
        return "cycle_3_general", ["niveau:cycle_3_general"], 35
    if cycle == "cycle_4":
        return "cycle_4_general", ["niveau:cycle_4_general"], 35
    return "inconnu", ["niveau:non_detecte"], 10


def infer_doc_type(relative_path: str, text: str) -> tuple[str, list[str], int]:
    primary = fold(relative_path)
    secondary = fold(text[:12000])
    scores: dict[str, int] = {}
    indices: list[str] = []
    for doc_type, terms in DOC_TYPE_TERMS.items():
        for term in terms:
            folded_term = fold(term)
            if folded_term in primary:
                scores[doc_type] = scores.get(doc_type, 0) + 3
            if folded_term in secondary:
                scores[doc_type] = scores.get(doc_type, 0) + 1
    if not scores:
        return "ressource_non_classee", ["type:non_classe"], 20
    doc_type, raw_score = sorted(scores.items(), key=lambda item: item[1], reverse=True)[0]
    for term in DOC_TYPE_TERMS[doc_type]:
        if fold(term) in primary or fold(term) in secondary:
            indices.append(f"type:{term}")
            if len(indices) >= 3:
                break
    return doc_type, indices, min(35 + raw_score * 12, 95)


def classify_row(row: dict[str, str], index_dir: Path) -> dict[str, str]:
    text_path = index_dir / row["TextFile"]
    text = text_path.read_text(encoding="utf-8", errors="replace") if text_path.exists() else ""
    relative_path = row["RelativePath"]

    cycle, cycle_indices, cycle_score = infer_cycle(relative_path, text)
    subject, subject_indices, subject_score = infer_subject(relative_path)
    level, level_indices, level_score = infer_level(relative_path, text, cycle)
    doc_type, type_indices, type_score = infer_doc_type(relative_path, text)

    score = round((cycle_score * 0.25) + (subject_score * 0.25) + (level_score * 0.2) + (type_score * 0.3))
    indices = cycle_indices + subject_indices + level_indices + type_indices

    return {
        "Id": row["Id"],
        "RelativePath": relative_path,
        "Cycle": cycle,
        "Matiere": subject,
        "Niveau": level,
        "TypeDocument": doc_type,
        "UsagePrioritaire": USAGE_BY_TYPE.get(doc_type, "a_trier"),
        "Score": str(score),
        "Indices": " | ".join(indices),
        "TextFile": row["TextFile"],
        "InfoFile": row["InfoFile"],
        "SHA256": row.get("SHA256", ""),
    }


def priority_rows(classified: list[dict[str, str]]) -> list[dict[str, str]]:
    type_weight = {
        "programme_officiel": 100,
        "attendus_reperes": 90,
        "guide_enseignant": 75,
        "activite_sequence": 65,
        "evaluation": 55,
        "exercice": 50,
        "ressource_non_classee": 10,
    }
    best_by_bucket: dict[tuple[str, str, str, str], list[dict[str, str]]] = defaultdict(list)
    for row in classified:
        bucket = (row["Cycle"], row["Matiere"], row["Niveau"], row["TypeDocument"])
        best_by_bucket[bucket].append(row)

    selected: list[dict[str, str]] = []
    for rows in best_by_bucket.values():
        rows.sort(
            key=lambda row: (
                type_weight.get(row["TypeDocument"], 0),
                int(row["Score"] or 0),
            ),
            reverse=True,
        )
        selected.extend(rows[:3])

    selected.sort(
        key=lambda row: (
            row["Cycle"],
            row["Matiere"],
            row["Niveau"],
            -type_weight.get(row["TypeDocument"], 0),
            -int(row["Score"] or 0),
        )
    )

    out: list[dict[str, str]] = []
    for index, row in enumerate(selected, start=1):
        out.append(
            {
                "PriorityRank": str(index),
                "Cycle": row["Cycle"],
                "Matiere": row["Matiere"],
                "Niveau": row["Niveau"],
                "TypeDocument": row["TypeDocument"],
                "UsagePrioritaire": row["UsagePrioritaire"],
                "Score": row["Score"],
                "Id": row["Id"],
                "RelativePath": row["RelativePath"],
                "Indices": row["Indices"],
            }
        )
    return out


def parse_args(argv: list[str]) -> argparse.Namespace:
    parser = argparse.ArgumentParser(description="Classe l'index PDF global.")
    parser.add_argument("--index-dir", type=Path, default=DEFAULT_INDEX)
    parser.add_argument("--out-dir", type=Path, default=DEFAULT_OUT)
    return parser.parse_args(argv)


def main(argv: list[str]) -> int:
    args = parse_args(argv)
    manifest = args.index_dir / "manifest.csv"
    rows = read_csv(manifest)
    classified = [classify_row(row, args.index_dir) for row in rows]
    priorities = priority_rows(classified)

    write_csv(args.out_dir / "classification.csv", CLASSIFICATION_FIELDS, classified)
    write_csv(args.out_dir / "sources_prioritaires.csv", PRIORITY_FIELDS, priorities)

    print(f"PDF classés: {len(classified)}")
    print(f"Sources prioritaires: {len(priorities)}")
    print(f"Classification: {args.out_dir / 'classification.csv'}")
    print(f"Sources: {args.out_dir / 'sources_prioritaires.csv'}")
    return 0


if __name__ == "__main__":
    raise SystemExit(main(sys.argv[1:]))
