"""naming — derive + apply the ParVagues loop naming convention (task #13).

Convention recovered empirically from the Samples corpus (1690 loops, 2026-06-23):

    NN_<role>[_<character>]          e.g. 00_crime_synth0, 03_keys_brass2_MINOR

  • NN  — a 2-digit index ordering loops within a kit (46% of the corpus carry it;
          it's the hand-cut-kit signature, vs keyed/tempo packs like `120g`/`80c`).
  • role — what it is, from the STEM not a guess (drums/bass/other/vocals, refined
          to the character tokens PLN actually uses: synth, keys, guitar, brass,
          voice…). Never infer role from the name — the finder knows the stem.
  • character — optional human descriptor (dark, intro, MINOR, explose…); a section
          tag (chorus/drop) is a good default when known.
  • underscore-joined (74%), lowercase, the kit name NOT repeated (only 16% do — the
          folder already carries it).

`suggest_name()` is what the GUI Forge button / export_take fill in; `lint()` flags
names that drift from the convention; `analyze_corpus()` reproduces the stats above
(parsers-over-copy — re-derive, don't trust this comment).
"""
from __future__ import annotations

import re
from collections import Counter
from pathlib import Path
from typing import Optional

# Canonical roles (the four stems) + the finer character tokens seen in the corpus,
# each mapped to its stem so a suggested name stays honest to what was separated.
ROLE_SYNONYMS = {
    "drums": "drums", "drum": "drums", "perc": "drums", "beat": "drums", "break": "drums",
    "bass": "bass", "sub": "bass", "808": "bass",
    "vocals": "vocals", "vocal": "vocals", "voice": "vocals", "vox": "vocals",
    "synth": "other", "keys": "other", "key": "other", "piano": "other",
    "organ": "other", "guitar": "other", "brass": "other", "pad": "other",
    "lead": "other", "chord": "other", "other": "other",
}
STEMS = ("drums", "bass", "other", "vocals")


def slug_token(s: str) -> str:
    """Lowercase, ascii-ish, strip to a single naming token."""
    s = re.sub(r"[^A-Za-z0-9]+", "", s.strip().lower())
    return s


def stem_of(role: str) -> str:
    """Map a role/character word to its stem family (honest to separation)."""
    return ROLE_SYNONYMS.get(slug_token(role), "other")


def suggest_name(index: int, role: str, *, character: Optional[str] = None,
                 section: Optional[str] = None, taken: Optional[set] = None) -> str:
    """Build a convention-compliant name: NN_<role>[_<section>][_<character>].

    `role` is the stem/character word (kept as given if it's already a known token,
    else the stem family). De-duplicates against `taken` by bumping a suffix.
    """
    parts = [f"{int(index):02d}", slug_token(role) or "loop"]
    if section:
        parts.append(slug_token(section))
    if character:
        parts.append(slug_token(character))
    name = "_".join(parts)
    if taken:
        base, k = name, 1
        while name in taken:
            name = f"{base}_{k}"
            k += 1
    return name


def lint(name: str) -> list[str]:
    """Flag drift from the convention. Empty list = clean."""
    issues = []
    stem = name[:-4] if name.lower().endswith(".wav") else name
    if not re.match(r"^\d{2}_", stem):
        issues.append("no NN_ index prefix")
    if " " in stem:
        issues.append("contains spaces")
    if stem != stem.lower():
        issues.append("not lowercase")
    if re.search(r"[^a-z0-9_]", stem.lower()):
        issues.append("non [a-z0-9_] characters")
    return issues


def analyze_corpus(samples_root: str | Path) -> dict:
    """Re-derive the naming stats from the live corpus (depth-1 loops per kit)."""
    root = Path(samples_root)
    names = []
    for kit in root.iterdir():
        if not kit.is_dir():
            continue
        for f in kit.iterdir():
            if f.is_file() and f.suffix.lower() == ".wav":
                names.append((kit.name, f.stem))
    n = len(names) or 1
    indexed = sum(1 for _, x in names if re.match(r"^\d{1,2}[_-]", x))
    under = sum(1 for _, x in names if "_" in x)
    kit_in = sum(1 for k, x in names if k.lower() in x.lower())
    toks: Counter = Counter()
    for _, x in names:
        body = re.sub(r"^\d{1,2}[_-]", "", x)
        for t in re.split(r"[_-]", body):
            t = re.sub(r"\d+$", "", t.lower())
            if len(t) >= 2:
                toks[t] += 1
    return {
        "n_loops": len(names),
        "pct_indexed": round(100 * indexed / n, 1),
        "pct_underscore": round(100 * under / n, 1),
        "pct_repeats_kit": round(100 * kit_in / n, 1),
        "top_role_tokens": toks.most_common(20),
    }


def _main(argv=None) -> int:
    import argparse
    import json
    import os
    ap = argparse.ArgumentParser(description="ParVagues loop naming convention")
    ap.add_argument("--analyze", metavar="SAMPLES_ROOT",
                    default=os.environ.get("FOUNDRY_SAMPLES",
                                           str(Path.home() / "Work/Sound/Samples")))
    ap.add_argument("--suggest", nargs="+", metavar="INDEX ROLE [CHAR]",
                    help="e.g. --suggest 3 synth dark")
    ap.add_argument("--lint", metavar="NAME")
    a = ap.parse_args(argv)
    if a.suggest:
        i, role = a.suggest[0], a.suggest[1]
        char = a.suggest[2] if len(a.suggest) > 2 else None
        print(suggest_name(int(i), role, character=char))
    elif a.lint:
        iss = lint(a.lint)
        print("clean" if not iss else "⚠ " + ", ".join(iss))
    else:
        print(json.dumps(analyze_corpus(a.analyze), indent=2))
    return 0


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