"""Rapport de couverture sonore: ce qui sonne, ce qui reste muet, et pourquoi.

Le rapport ne juge pas le RENDU — aucun clip n'a ete ecoute. Il repond a une
seule question verifiable: pour chaque evenement du contrat, une configuration
est-elle atteignable, et d'ou vient-elle.

Usage: python tools/audio_coverage.py
"""

from __future__ import annotations

import io
import json
from collections import defaultdict
from pathlib import Path

PROJECT = Path(__file__).resolve().parents[1]
MASTER = PROJECT / "data" / "game_data_master.json"
CONTRACT = PROJECT / "data" / "audio_contract.json"
OUT = PROJECT / "docs" / "audio" / "couverture.md"


def load(path: Path) -> dict:
    return json.loads(io.open(path, encoding="utf-8").read())


def is_set(cue) -> bool:
    return isinstance(cue, dict) and cue.get("mode") in ("custom", "silent")


def main() -> int:
    master = load(MASTER)
    contract = load(CONTRACT)
    events = contract["events"]
    scopes = contract["scopes"]
    clips = master.get("audio_clips", {}) or {}
    defaults = master.get("audio_defaults", {}) or {}
    default_events = defaults.get("events", {}) or {}

    # Qui surcharge quoi, par evenement global.
    overrides: dict[str, list[str]] = defaultdict(list)

    def note(scope: str, sounds, label: str) -> None:
        prefix = scopes[scope]["prefix"]
        for key, cue in (sounds or {}).items():
            if is_set(cue):
                overrides[prefix + key].append(label)

    for pid, item in (master.get("player_configs") or {}).items():
        note("player_configs", item.get("sounds"), f"joueur {pid}")
    for wid, item in (master.get("weapons") or {}).items():
        note("weapons", item.get("sounds"), f"arme {wid}")
    for eid, enemy in (master.get("enemies") or {}).items():
        note("enemies", enemy.get("sounds"), f"monstre {eid}")
        for index, attack in enumerate(enemy.get("attacks") or []):
            note("enemy_attacks", attack.get("sounds"), f"{eid} attaque {index + 1}")
        for level_index, level in enumerate(enemy.get("levels") or []):
            note("enemies", level.get("sounds"), f"{eid} niveau {level_index + 2}")
            for index, attack in enumerate(level.get("attacks") or []):
                note("enemy_attacks", attack.get("sounds"),
                     f"{eid} niveau {level_index + 2} attaque {index + 1}")
    for bid, item in (master.get("biomes") or {}).items():
        note("biomes", item.get("sounds"), f"biome {bid}")
    for rid, item in (master.get("rooms") or {}).items():
        note("rooms", item.get("sounds"), f"salle {rid}")

    covered, missing = [], []
    for key, info in events.items():
        has_default = is_set(default_events.get(key))
        if has_default or overrides.get(key):
            covered.append((key, info, has_default, overrides.get(key, [])))
        else:
            missing.append((key, info))

    # Canaux continus.
    channel_rows = []
    for context in contract["contexts"]:
        entry = (defaults.get("contexts") or {}).get(context, {}) or {}
        channel_rows.append((f"contexte {context}",
                             is_set(entry.get("music")), is_set(entry.get("ambience"))))
    for bid, biome in (master.get("biomes") or {}).items():
        audio = biome.get("audio") or {}
        channel_rows.append((f"biome {bid}", is_set(audio.get("music")), is_set(audio.get("ambience"))))

    used = set()
    for cue in list(default_events.values()):
        for variant in (cue.get("variants") or []) if isinstance(cue, dict) else []:
            used.add(variant.get("clip"))
    for context in (defaults.get("contexts") or {}).values():
        for cue in context.values():
            for variant in (cue.get("variants") or []) if isinstance(cue, dict) else []:
                used.add(variant.get("clip"))
    for bid, biome in (master.get("biomes") or {}).items():
        for cue in (biome.get("audio") or {}).values():
            for variant in (cue.get("variants") or []) if isinstance(cue, dict) else []:
                used.add(variant.get("clip"))
    for wid, weapon in (master.get("weapons") or {}).items():
        for cue in (weapon.get("sounds") or {}).values():
            for variant in (cue.get("variants") or []) if isinstance(cue, dict) else []:
                used.add(variant.get("clip"))
    unused = sorted(set(clips) - used)

    lines = [
        "# Couverture sonore",
        "",
        "Genere par `python tools/audio_coverage.py`. **Aucun clip n'a ete ecoute** :",
        "ce rapport dit ce qui est ATTEIGNABLE, pas ce qui sonne juste.",
        "",
        f"- Evenements au contrat : **{len(events)}**",
        f"- Couverts : **{len(covered)}**  ·  sans configuration : **{len(missing)}**",
        f"- Clips importes : **{len(clips)}**  ·  jamais affectes : **{len(unused)}**",
        "",
        "## Evenements couverts",
        "",
        "| Evenement | Defaut general | Surcharges |",
        "| --- | --- | --- |",
    ]
    for key, info, has_default, sources in sorted(covered):
        marks = "oui" if has_default else "—"
        detail = ", ".join(sources[:4]) + ("…" if len(sources) > 4 else "") if sources else "—"
        lines.append(f"| `{key}` — {info['label']} | {marks} | {detail} |")

    lines += ["", "## Evenements encore muets", "",
              "Aucun candidat credible dans la banque fournie. Le point d'accroche",
              "existe dans le jeu : il suffira d'affecter un son dans le gestionnaire.",
              "", "| Evenement | Quand il se declenche |", "| --- | --- |"]
    for key, info in sorted(missing):
        lines.append(f"| `{key}` — {info['label']} | {info.get('hint', '')} |")

    lines += ["", "## Musique et ambiance", "",
              "| Profil | Musique | Ambiance |", "| --- | --- | --- |"]
    for label, music, ambience in channel_rows:
        lines.append(f"| {label} | {'oui' if music else '—'} | {'oui' if ambience else '—'} |")

    if unused:
        lines += ["", "## Clips importes mais jamais affectes", "",
                  "Ils restent disponibles dans le catalogue du gestionnaire.", ""]
        lines += [f"- `{clip}`" for clip in unused]

    OUT.parent.mkdir(parents=True, exist_ok=True)
    OUT.write_text("\n".join(lines) + "\n", encoding="utf-8")
    print(f"{len(covered)}/{len(events)} evenements couverts, {len(unused)} clips inutilises -> {OUT}")
    return 0


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