"""Scoring 4Big (§7 du template canonique) + dérivation du statut faisabilité.

Barème (20 pts × 5 axes = 100) — reproduit fidèlement §7 :
  1. Complétude          — 4 volets présents + champs 🔴 remplis à 100 %
  2. Anti-gap prix       — tableau typologies §3.2 complet (aucun prix manquant)
  3. Traçabilité         — chiffres sourcés (sources[] + taux de conversion daté)
  4. Paramètres canon.   — 3 % / 8.5 % / 52 % · USD+DOP · Letter US · Cardnet
  5. Machine-lisible     — version.json valide + extraction Publiciste OK

Axes 1-4 : calculés à partir du brief (déterministe, sans invention #6).
Axe 5    : GARANTI PAR CONSTRUCTION (le renderer produit une arborescence
           conforme au parser + un version.json conforme au schéma). Le CLI le
           PROUVE après génération (re-parse + validation) et échoue bruyamment
           sinon — le score n'est donc jamais auto-décerné à l'aveugle.
"""

from __future__ import annotations

from typing import Any

from . import model

_SEUIL_PUBLIABLE = 95  # CLAUDE.md #5 · version.schema.json (complete ⇒ score>=95)


def _score_completude(brief: dict) -> tuple[int, list[str], list[str]]:
    """Axe 1 → (points/20, volets_complets, champs_manquants).

    Un volet est « complet » si TOUS ses champs 🔴 sont remplis. Pour
    l'architecture, la présence d'au moins une typologie est aussi exigée.
    """
    total = 0
    filled = 0
    champs_manquants: list[str] = []
    volets_complets: list[str] = []

    for volet in model.VOLETS:
        section = brief.get(_brief_key(volet))
        volet_ok = True
        for champ in model.REQUIRED[volet]:
            total += 1
            if model.is_filled(model.get(section, champ)):
                filled += 1
            else:
                volet_ok = False
                champs_manquants.append(f"{volet}.{champ}")
        # L'architecture exige aussi au moins une typologie renseignée (§3.2).
        if volet == "architecture":
            typos = model.get(section, "typologies") or []
            if not typos:
                volet_ok = False
                if "architecture.typologies" not in champs_manquants:
                    champs_manquants.append("architecture.typologies")
        if volet_ok:
            volets_complets.append(volet)

    pts = round(20 * filled / total) if total else 0
    return pts, volets_complets, champs_manquants


def _score_anti_gap_prix(brief: dict) -> tuple[int, list[str]]:
    """Axe 2 → (points/20, champs_manquants typologies).

    Chaque typologie doit renseigner ses 7 cellules 🔴 (§3.2). Le score est la
    fraction de cellules 🔴 réellement remplies ; 0 s'il n'existe aucune typologie.
    """
    archi = brief.get("architecture") or {}
    typos = archi.get("typologies") or []
    if not typos:
        return 0, ["architecture.typologies (aucune typologie — bloc anti-gap vide)"]

    total = len(typos) * len(model.TYPO_REQUIRED)
    filled = 0
    manquants: list[str] = []
    for i, typo in enumerate(typos):
        for champ in model.TYPO_REQUIRED:
            if model.is_filled(model.get(typo, champ)):
                filled += 1
            else:
                manquants.append(f"architecture.typologies[{i}].{champ}")
    pts = round(20 * filled / total) if total else 0
    return pts, manquants


def _score_tracabilite(brief: dict) -> tuple[int, list[str]]:
    """Axe 3 → (points/20, remarques).

    10 pts : `sources[]` non vide (chaque chiffre rattachable à une archive).
    10 pts : `architecture.taux_conversion` avec `date` ET `source` (règle §3.2 :
             le taux USD↔DOP doit être daté et sourcé — jamais inventé).
    """
    pts = 0
    remarques: list[str] = []
    if model.is_filled(brief.get("sources")):
        pts += 10
    else:
        remarques.append("sources[] absent — traçabilité des chiffres non établie")

    taux = (brief.get("architecture") or {}).get("taux_conversion") or {}
    if model.is_filled(taux.get("date")) and model.is_filled(taux.get("source")):
        pts += 10
    else:
        remarques.append("architecture.taux_conversion.{date,source} incomplet (§3.2)")
    return pts, remarques


def _score_parametres_canoniques() -> int:
    """Axe 4 → 20/20. Garanti : le renderer émet toujours les 6 paramètres
    canoniques (3 % / 8.5 % / 52 % · USD+DOP · Letter US · Cardnet).
    Le CLI vérifie leur présence dans le rendu (`renderer.check_canonical`)."""
    return 20


def score(brief: dict, machine_lisible_pts: int = 20) -> dict[str, Any]:
    """Calcule le bilan 4Big complet + le statut faisabilité.

    `machine_lisible_pts` (axe 5) vaut 20 par construction ; le CLI le remplace
    par le résultat réel de la re-validation après génération.
    """
    pts1, volets_complets, champs1 = _score_completude(brief)
    pts2, champs2 = _score_anti_gap_prix(brief)
    pts3, remarques3 = _score_tracabilite(brief)
    pts4 = _score_parametres_canoniques()

    champs_manquants = _dedupe(champs1 + champs2)
    total = pts1 + pts2 + pts3 + pts4 + machine_lisible_pts

    statut = _statut(total, champs_manquants, volets_complets, pts2)

    return {
        "score_4big": total,
        "statut_faisabilite": statut,
        "volets_complets": volets_complets,
        "champs_manquants": champs_manquants,
        "axes": {
            "completude": pts1,
            "anti_gap_prix": pts2,
            "tracabilite": pts3,
            "parametres_canoniques": pts4,
            "machine_lisible": machine_lisible_pts,
        },
        "remarques": remarques3,
    }


def _statut(total: int, champs_manquants: list[str], volets_complets: list[str],
            pts_prix: int) -> str:
    """« complete » exige le seuil publiable (#5) ET aucun champ 🔴 manquant ET
    le bloc prix intégral ET les 4 volets — cohérent avec version.schema.json.
    Sinon « en_developpement », ou « incomplete » si aucun volet abouti."""
    prix_complet = pts_prix >= 20
    if (
        total >= _SEUIL_PUBLIABLE
        and not champs_manquants
        and prix_complet
        and len(volets_complets) >= 4
    ):
        return "complete"
    if not volets_complets:
        return "incomplete"
    return "en_developpement"


def _brief_key(volet: str) -> str:
    """Le brief utilise « paysage » pour le volet « paysage_experience »."""
    return "paysage" if volet == "paysage_experience" else volet


def _dedupe(items: list[str]) -> list[str]:
    seen: set[str] = set()
    out: list[str] = []
    for it in items:
        if it not in seen:
            seen.add(it)
            out.append(it)
    return out
