#!/usr/bin/env python3
"""Score VoxStudio 'with speaker labels' TXT exports against the composite ground truth.

Method (utterance level):
1. Each exported block is "Speaker N: text". Blocks may hold several utterances.
2. Each ground-truth utterance is assigned to the export block that covers most of its
   normalised characters (longest-match coverage).
3. Export labels are mapped one-to-one to ground-truth voices by the best permutation
   (this is generous to the app: it is not told the mapping).
4. Reported: utterances whose mapped label matches, label count, and block purity
   (share of a block's characters that come from its majority voice).
"""
import difflib
import json
import re
import sys
from itertools import permutations

def norm(s):
    s = s.lower()
    return re.sub(r"[\s\W_]+", "", s, flags=re.UNICODE)

def parse_blocks(path):
    blocks = []
    for line in open(path, encoding="utf-8"):
        line = line.strip()
        m = re.match(r"^Speaker (\d+):\s*(.*)$", line)
        if m:
            blocks.append({"label": int(m.group(1)), "text": m.group(2)})
    return blocks

def coverage(utt_norm, block_norm):
    sm = difflib.SequenceMatcher(None, utt_norm, block_norm, autojunk=False)
    matched = sum(b.size for b in sm.get_matching_blocks())
    return matched / max(1, len(utt_norm))

def score(manifest_path, export_path):
    m = json.load(open(manifest_path, encoding="utf-8"))
    utts = []
    for t in m["turns"]:
        for u in t["utterances"]:
            utts.append({"speaker": t["speaker"], "text": u["text"], "turn_start": t["start"]})
    blocks = parse_blocks(export_path)
    bnorm = [norm(b["text"]) for b in blocks]
    assigned = []
    for u in utts:
        un = norm(u["text"])
        scores = [coverage(un, bn) for bn in bnorm]
        best = max(range(len(blocks)), key=lambda i: scores[i])
        assigned.append({"speaker": u["speaker"], "block": best, "label": blocks[best]["label"],
                         "cov": round(scores[best], 2), "text": u["text"][:24]})
    labels = sorted({b["label"] for b in blocks})
    voices = sorted({u["speaker"] for u in utts})
    best_perm, best_correct = None, -1
    for perm in permutations(voices, len(labels)) if len(labels) <= len(voices) else []:
        mapping = dict(zip(labels, perm))
        correct = sum(1 for a in assigned if mapping.get(a["label"]) == a["speaker"])
        if correct > best_correct:
            best_correct, best_perm = correct, mapping
    # Purity per block: share of matched utterance characters from the block's majority voice.
    purity = []
    for i, b in enumerate(blocks):
        mine = [a for a in assigned if a["block"] == i]
        counts = {}
        for a in mine:
            counts[a["speaker"]] = counts.get(a["speaker"], 0) + len(norm(a["text"]))
        total = sum(counts.values())
        purity.append(round(max(counts.values()) / total, 2) if total else None)
    return {
        "export": export_path.split("/")[-1],
        "n_utterances": len(utts),
        "n_blocks": len(blocks),
        "n_labels": len(labels),
        "n_voices": len(voices),
        "best_mapping": best_perm,
        "correct_utterances": best_correct,
        "utterance_accuracy": round(best_correct / len(utts), 3),
        "block_purity": purity,
        "per_utterance": assigned,
    }

if __name__ == "__main__":
    manifest, export = sys.argv[1], sys.argv[2]
    res = score(manifest, export)
    out = {k: v for k, v in res.items() if k != "per_utterance"}
    print(json.dumps(out, ensure_ascii=False, indent=2))
    if "--detail" in sys.argv:
        for a in res["per_utterance"]:
            print(a)
