#!/usr/bin/env python3
"""Match accepted ASR transcripts to known Godot voice catalog constants.

This does not prove a native/Radium symbol-to-object map. It is a conservative
triage layer: when a transcript exactly matches a known `SE_PK_VO_*` catalog
phrase or a narrow, documented ASR error variant, the row is promoted from a
free-text ASR slug to a catalog-style inferred name.
"""

from __future__ import annotations

import argparse
import csv
import json
import re
import unicodedata
from pathlib import Path


FIELDS = [
    "sound_name",
    "wav_path",
    "section8_index",
    "object_id",
    "duration_seconds",
    "channels",
    "catalog_constant",
    "match_phrase",
    "match_kind",
    "transcript",
    "transcript_norm",
    "avg_logprob",
    "no_speech_prob",
    "compression_ratio",
    "model",
]


PHRASE_OVERRIDES = {
    "SE_PK_VO_COUNTDOWN": [
        "ten",
        "10",
        "nine",
        "9",
        "eight",
        "8",
        "seven",
        "7",
        "six",
        "6",
        "five",
        "5",
        "four",
        "4",
        "three",
        "3",
        "two",
        "2",
        "one",
        "1",
    ],
    "SE_PK_VO_TIMES_UP": ["time is up", "times up"],
    "SE_PK_VO_HIGH_SCORE": ["high score"],
    "SE_PK_VO_ENTER_YOUR_INITIALS": ["enter your initials"],
    "SE_PK_VO_REPLAY": ["replay"],
    "SE_PK_VO_BALL_SAVE": ["ball save"],
    "SE_PK_VO_EXTRA_BALL_LIT": ["extra ball is lit", "extra ball lit"],
    "SE_PK_VO_EXTRA_BALL": [
        "extra ball",
        "extra bow",
        "get the extra ball",
        "shoot the town scoop to collect the extra ball",
    ],
    "SE_PK_VO_SHOOT_AGAIN": ["shoot again"],
    "SE_PK_VO_SKILL_SHOT": ["skill shot"],
    "SE_PK_VO_SUPER_SKILL_SHOT": ["super skill shot"],
    "SE_PK_VO_SUPER_SECRET_SKILL_SHOT": ["super secret skill shot"],
    "SE_PK_VO_JACKPOT": ["jackpot", "jetpot"],
    "SE_PK_VO_DOUBLE_JACKPOT": ["double jackpot"],
    "SE_PK_VO_TRIPLE_JACKPOT": ["triple jackpot"],
    "SE_PK_VO_SUPER_JACKPOT": ["super jackpot"],
    "SE_PK_VO_DOUBLE_SUPER_JACKPOT": ["double super jackpot"],
    "SE_PK_VO_DISCOVER_POKEMON_DISCOVERED": ["pokemon discovered"],
    "SE_PK_VO_DISCOVER_INSTRUCTIONS": [
        "shoot pokedex to scan pokemon",
        "shoot polka decks to scan polka man",
    ],
    "SE_PK_VO_DISCOVER_POKEMON_SCANNED": ["pokemon scanned"],
    "SE_PK_VO_CATCH_INSTRUCTIONS": ["shoot the pokeball to catch the pokemon"],
    "SE_PK_VO_CATCH_POKEBALL_LIT_INSTRUCTIONS": ["hit purple shots to light the pokeball"],
    "SE_PK_VO_CATCH_POKEMON_CAUGHT": ["pokemon caught"],
    "SE_PK_VO_CATCH_POKEMON_ESCAPED": ["pokemon has escaped", "the pokemon has escaped"],
    "SE_PK_VO_RIVAL_BATTLE_LIT_INSTRUCTIONS": ["shoot the town scoop to start a rival battle"],
    "SE_PK_VO_RIVAL_BATTLE_LIT_SHORT": ["rival battle is lit"],
    "SE_PK_VO_WAITING_FOR_PLUNGE": [
        "plunge the ball to play",
        "blanche the ball to play",
        "plants the ball to play",
    ],
    "SE_PK_VO_PLAYER_1_UP_FIRST_BALL": ["trainer 1 is up"],
    "SE_PK_VO_PLAYER_1_UP_SECOND_BALL_ONWARDS": [
        "trainer ones adventure continues",
        "trainer 1s adventure continues",
    ],
    "SE_PK_VO_PLAYER_2_ADDED": ["trainer 2"],
    "SE_PK_VO_PLAYER_2_UP_FIRST_BALL": ["trainer 2 is up"],
    "SE_PK_VO_PLAYER_2_UP_SECOND_BALL_ONWARDS": [
        "trainer2s adventure continues",
        "trainer 2s adventure continues",
    ],
    "SE_PK_VO_PLAYER_3_ADDED": ["trainer 3"],
    "SE_PK_VO_PLAYER_3_UP_FIRST_BALL": ["trainer 3 is up"],
    "SE_PK_VO_PLAYER_3_UP_SECOND_BALL_ONWARDS": ["trainer 3s adventure continues"],
    "SE_PK_VO_PLAYER_4_ADDED": ["trainer 4"],
    "SE_PK_VO_PLAYER_4_UP_FIRST_BALL": ["trainer 4 is up"],
    "SE_PK_VO_PLAYFIELD_X_START": ["all scoring is doubled"],
    "SE_PK_VO_GAME_START": [
        "ready set adventure",
        "today is no ordinary day today the adventure begins",
    ],
    "SE_PK_VO_OUTLANE_BALL_SAVE_INSTRUCTIONS": ["hit the town targets to light the outlane ball save"],
    "SE_PK_VO_MYSTERY_LIT": ["shoot the town scoop to collect a mystery award"],
    "SE_PK_VO_STORY_MODE_GENERIC_OUTRO": [
        "no matter the outcome the trainer and pokemon will stand side by side in the quest to become champion of the arena",
        "no matter the outcome the trainer and pokemon will stand side by side in the quest to become the champion of the arena",
        "no matter the outcome the trainer and pokemon will stand side by side in the quest to become champion of the arena",
    ],
    "SE_PK_VO_BULBASAUR_1_INTRO": ["bulbasaur story begins", "bulbasaurs story begins", "bobosaurus story begins"],
    "SE_PK_VO_BULBASAUR_1_OUTRO": ["bulbasaur story continues", "bulbasar story continues", "bobosaurus story continues"],
    "SE_PK_VO_SQUIRTLE_1_INTRO": [
        "squirtle story begins",
        "squirtled starry starts here",
        "squirrel story starts here",
        "squirtled story starts here",
        "time for a squirtle squad adventure",
        "time for a squirtle squirted adventure",
    ],
    "SE_PK_VO_SQUIRTLE_1_OUTRO": [
        "squirtle story continues",
        "the squirtle squad has saved the day just in time for the next adventure",
        "the squirtled squad has saved the day just in time for the next adventure",
        "once again the squirtle squad has saved the day",
        "once again the squirtled squad has saved the day",
    ],
    "SE_PK_VO_CHRMANDER_1_INTRO": ["charmander story begins", "charmanders story begins"],
    "SE_PK_VO_CHRMANDER_1_OUTRO": ["charmander story continues"],
    "SE_PK_VO_BATTLE_TRAINER_CHOOSES": [
        "trainer 1 chooses",
        "trainer 2 chooses",
        "trainer 3 chooses",
        "trainer 4 chooses",
    ],
    "SE_PK_VO_BATTLE_COMMENTARY_AMAZED": ["an amazing score"],
    "SE_PK_VO_BATTLE_COMMENTARY_AFTER_BATTLE": ["what a showdown", "a real showdown"],
    "SE_PK_VO_BATTLE_COMMENTARY_COMEBACK": ["unbelievable comeback", "what a comeback"],
    "SE_PK_VO_BATTLE_COMMENTARY_DODGE": ["cant touch this"],
    "SE_PK_VO_BATTLE_COMMENTARY_ELECTRIC": ["its electric", "that electric attacks a real shocker"],
    "SE_PK_VO_BATTLE_COMMENTARY_FIRE": [
        "is it just me or is it getting hot in here",
        "its about to get hot in here",
    ],
    "SE_PK_VO_BATTLE_COMMENTARY_FLYING": ["air glide", "gliding through the trees"],
    "SE_PK_VO_BATTLE_COMMENTARY_GRASS": ["check out that grass attack"],
    "SE_PK_VO_BATTLE_COMMENTARY_GENERIC_ATTACK": ["great move"],
    "SE_PK_VO_BATTLE_COMMENTARY_GROUND": [
        "earthy shove",
        "great tunneling burst",
        "that is a clean ground strike",
    ],
    "SE_PK_VO_BATTLE_COMMENTARY_ICE": ["cool strike", "frost flash", "shilling shift"],
    "SE_PK_VO_BATTLE_COMMENTARY_INCOMING": ["incoming"],
    "SE_PK_VO_BATTLE_COMMENTARY_LAST_POKEMON": ["looks like the rival is down to only one pokemon"],
    "SE_PK_VO_BATTLE_COMMENTARY_ROCK": ["rock solid"],
    "SE_PK_VO_BATTLE_COMMENTARY_STEEL": ["iron impact", "metallic blast"],
    "SE_PK_VO_BATTLE_COMMENTARY_WATER": ["nice splash", "smooth ripple"],
    "SE_PK_VO_ABRA": ["abra", "oh its abra"],
    "SE_PK_VO_ABRA_COMMENTARY": [
        "abra blinks across the sand with perfect timing",
        "look at that precision flash",
        "swift vanish into thin air",
    ],
    "SE_PK_VO_ABRA_PIKACHU": ["pikachu and abra face off in a fast paced test of spark and sudden psychic shifts"],
    "SE_PK_VO_CATERPIE": ["caterpie", "oh its canopy"],
    "SE_PK_VO_CATERPIE_COMMENTARY": [
        "caterpies tiny but this moment is huge",
        "nice move right through the leaves",
        "theres that classic caterpie burst",
    ],
    "SE_PK_VO_CATERPIE_PIKACHU": [
        "its pikachu vs caterpie speed meets spirit in a classic forest matchup",
        "pikachu vs caterpie spark meets spirit beneath the treetops",
    ],
    "SE_PK_VO_ODDISH": ["oh its irish"],
    "SE_PK_VO_ODDISH_COMMENTARY": [
        "great burst from the little leaf star",
        "onish getting back to its roots",
        "what a move fresh from the forest floor",
    ],
    "SE_PK_VO_ODDISH_PIKACHU": ["its pikachu and audish electric speed meets fresh forest charm"],
    "SE_PK_VO_SCYTHER": ["oh its seifer"],
    "SE_PK_VO_SCYTHER_COMMENTARY": [
        "brought smooths with quiet precision",
        "blinding speed",
        "cypher brings pure forest focus",
        "slicing through the air what precision",
        "those wings hum with every move",
    ],
    "SE_PK_VO_SCYTHER_PIKACHU": ["pikachu takes on sither fast sparks versus forest precision"],
    "SE_PK_VO_VENONAT": ["oh its venonat"],
    "SE_PK_VO_VENONAT_COMMENTARY": [
        "then and that hops in with those big bright eyes ready for action",
        "then a nut makes its move",
        "then in that smooth sparkle like dioncie",
        "what a sparkling move",
    ],
    "SE_PK_VO_VENONAT_PIKACHU": ["pikachu squares off with venonet lightning focus against big bright energy"],
    "SE_PK_VO_FOREST_BATTLE_INTRO": [
        "a rival trainer enters the forest",
        "and into the force battle we go",
        "deep in the heart of the forest legends are made",
        "moving on to the next battle against the forest rival",
        "welcome to the forest",
        "welcome to the forest where b drill buzz and leafy on lurk",
    ],
    "SE_PK_VO_FOREST_BATTLE_OUTRO": [
        "and thats a wrap here at the forest battle the trainers pokemon team had something to prove here and prove they did",
        "the adrenaline was bouncing off the branches in that forest battle but the trainers got no time to rest on their laurels if they",
        "true each match brings them one step closer to the gates of the pokemon arena but are they ready",
    ],
    "SE_PK_VO_LAKE_BATTLE_INTRO": [
        "a rival trainer approaches the lake",
        "its the quake by the lake folks",
        "moving on to the next battle against the lake rival",
        "that lake quake just happened time to quest on towards the pokemon arena",
        "the lake is calm but this battle is anything but the trainer and team are ready to clash with the rival and make some waves",
        "welcome to the lake",
        "where lakeside where the water shimmers and every ripple cheers on the challenger",
    ],
    "SE_PK_VO_LAKE_BATTLE_OUTRO": [
        "and thats how you battle at the lake folks one lake battle finished and more adventures to go on the road to the pokemon arena",
        "make no mistake for honor for friendship and for loyalty the lake battle goes to the challenger",
        "splash down by the lake and white clean and thats how the showdown goes down this trainer team just might be bound for the poke",
        "that was a doozy remember match by match our trainer and pokemon in one step closer to the pokemon arena",
    ],
    "SE_PK_VO_MOUNTAIN_BATTLE_INTRO": [
        "a rival trainer arrives at the mountain",
        "all bonds strengthen through struggle as the mountain battle begins",
        "buckle up for a wild ride at the majestic mountain its not every day you get to see a trainer and poke him on partner team of su",
        "each win brings them closer to the pokemon arena battle prize thats where skills and teamwork really shine where legends are ma",
        "its mountain mayhem folks welcome to the peak of competition",
        "moving on to the next battle against the mountain rival",
        "strategy patience and empathy will all be tested as the mountain battle begins",
        "they fought the wins and altitude to get this far",
        "welcome to the mountain",
    ],
    "SE_PK_VO_MOUNTAIN_BATTLE_OUTRO": [
        "merciless day on the mountain folks yes indeed this trainer and team just might be champion material",
        "theyre tearing up terrain here at the mountain battle you saw the hillside come alive with your own eyes just another dust up o",
    ],
    "SE_PK_VO_DESERT_BATTLE_INTRO": [
        "a rival trainer approaches the desert",
        "moving on to the next panel against the desert rival",
        "the trainer team is about to unleash the heat on the rival in this desert battle",
        "time to kick up some hot sand in the desert battle",
        "welcome to the desert",
        "will the rivals stand their ground or will the trainer team bring the heat",
    ],
    "SE_PK_VO_DESERT_BATTLE_OUTRO": [
        "with each match they press on to their destiny the pokemon arena",
        "and one teen spirits soar while anothers hopes are dashed on the desert floor down in the dirt with dugdrio",
        "sand is flying everyones pumped and the desert echoes with the sounds of victory",
        "that desert battle was a dustup",
        "that desert battle was prettier than a bramble guest",
        "these trainer and pokemon teams just get more impressive with every match a few more battles under their belts and they may just",
    ],
    "SE_PK_VO_TEAM_ROCKET_MULTIBALL_1_START": ["team rocket multiball", "team rocket montyball"],
}


def normalize(value: str) -> str:
    text = unicodedata.normalize("NFKD", value or "").encode("ascii", "ignore").decode("ascii")
    text = re.sub(r"['`]", "", text)
    text = re.sub(r"[^a-zA-Z0-9]+", " ", text).lower()
    return re.sub(r"\s+", " ", text).strip()


def read_tsv(path: Path) -> list[dict[str, str]]:
    with path.open(newline="", encoding="utf-8") as f:
        return list(csv.DictReader(f, delimiter="\t"))


def build_phrase_map(constants: list[str]) -> dict[str, list[tuple[str, str]]]:
    phrase_map: dict[str, list[tuple[str, str]]] = {}
    for constant in constants:
        if not constant.startswith("SE_PK_VO_"):
            continue
        phrases: list[tuple[str, str]] = []
        for phrase in PHRASE_OVERRIDES.get(constant, []):
            phrases.append((normalize(phrase), "override_exact"))
        suffix = constant.removeprefix("SE_PK_VO_").replace("CHRMANDER", "CHARMANDER")
        generated = normalize(suffix.replace("_", " "))
        if generated:
            phrases.append((generated, "suffix_exact"))
        deduped: dict[str, str] = {}
        for phrase, kind in phrases:
            deduped.setdefault(phrase, kind)
        phrase_map[constant] = [(phrase, kind) for phrase, kind in deduped.items()]
    return phrase_map


def best_match(transcript: str, phrase_map: dict[str, list[tuple[str, str]]]) -> tuple[str, str, str] | None:
    transcript_norm = normalize(transcript)
    matches: list[tuple[str, str, str]] = []
    for constant, phrases in phrase_map.items():
        for phrase, kind in phrases:
            if transcript_norm == phrase:
                matches.append((constant, phrase, kind))
    if not matches:
        return None
    return sorted(matches, key=lambda item: (len(item[1].split()), len(item[1])), reverse=True)[0]


def main() -> int:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--constant-map", type=Path, default=Path("analysis/godot-introspection/spike_sound_constant_map.json"))
    parser.add_argument("--asr-manifest", type=Path, default=Path("analysis/radium-image/asr_draft_sound_labels.tsv"))
    parser.add_argument("--best-manifest", type=Path, default=Path("analysis/all-sound-best-name-manifest.tsv"))
    parser.add_argument("--out", type=Path, default=Path("analysis/radium-image/asr_catalog_sound_label_candidates.tsv"))
    args = parser.parse_args()

    constants = list(json.loads(args.constant_map.read_text(encoding="utf-8")).keys())
    phrase_map = build_phrase_map(constants)
    current_best_source_by_wav = {
        row["source_wav_path"]: row["name_source"]
        for row in read_tsv(args.best_manifest)
        if row.get("bank") == "main"
    }
    rows: list[dict[str, str]] = []
    for row in read_tsv(args.asr_manifest):
        if row.get("accepted") != "yes":
            continue
        if current_best_source_by_wav.get(row["wav_path"]) not in {"asr_draft", "catalog_asr_match"}:
            continue
        match = best_match(row["transcript"], phrase_map)
        if not match:
            continue
        constant, phrase, kind = match
        rows.append(
            {
                "sound_name": row["sound_name"],
                "wav_path": row["wav_path"],
                "section8_index": row["section8_index"],
                "object_id": row["object_id"],
                "duration_seconds": row["duration_seconds"],
                "channels": row["channels"],
                "catalog_constant": constant,
                "match_phrase": phrase,
                "match_kind": kind,
                "transcript": row["transcript"],
                "transcript_norm": normalize(row["transcript"]),
                "avg_logprob": row["avg_logprob"],
                "no_speech_prob": row["no_speech_prob"],
                "compression_ratio": row["compression_ratio"],
                "model": row["model"],
            }
        )

    args.out.parent.mkdir(parents=True, exist_ok=True)
    with args.out.open("w", newline="", encoding="utf-8") as f:
        writer = csv.DictWriter(f, fieldnames=FIELDS, delimiter="\t")
        writer.writeheader()
        writer.writerows(sorted(rows, key=lambda item: int(item["section8_index"])))
    print(f"wrote {args.out} rows={len(rows)}")
    return 0


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