"""ReaScript generator — writes self-contained Python ReaScripts for REAPER."""

from __future__ import annotations

from pathlib import Path

from .commands import ReaScriptCommand


#: Known action names and their ordering in the dispatch loop.
_KNOWN_ACTIONS: frozenset[str] = frozenset([
    "add_plugins",
    "configure_fx_params",
    "verify_fx",
    "calibrate",
    "render",
])


class ReaScriptGenerator:
    """Generate a self-contained Python ReaScript file."""

    def generate(self, path: Path, command: ReaScriptCommand) -> None:
        """Write a self-contained ReaScript .py to path that REAPER can execute."""
        code = self._build_script(command)
        with open(path, "w", encoding="utf-8") as f:
            f.write(code)

    # --------------------------------------------------------------------
    # Hand-rolled JSON parser (~20 lines, no import json)
    # ReaScript Python has no json module; we parse via string splitting.
    # --------------------------------------------------------------------
    JSON_PARSER_SRC = """\
def parse_json(s):
    s = s.strip()
    if s[0] != "{" or s[-1] != "}":
        raise ValueError("not a JSON object")
    s = s[1:-1].strip()
    if not s:
        return {}
    result = {}
    segments = []
    depth = 0
    in_string = False
    escape = False
    start = 0
    i = 0
    while i < len(s):
        c = s[i]
        if not in_string and c in "{[":
            depth += 1
        elif not in_string and c in "}]":
            depth -= 1
        elif c == "\\\\":
            escape = not escape
        elif not escape and c == '"':
            in_string = not in_string
        elif not in_string and depth == 0 and c == ",":
            segments.append(s[start:i].strip())
            start = i + 1
        i += 1
    if start < len(s):
        segments.append(s[start:].strip())
    for seg in segments:
        colon_idx = -1
        in_str = False
        esc = False
        for j, ch in enumerate(seg):
            if ch == "\\\\":
                esc = not esc
            elif not esc and ch == '"':
                in_str = not in_str
            elif not in_str and ch == ":":
                colon_idx = j
                break
        if colon_idx < 0:
            continue
        key = seg[:colon_idx].strip()
        val = seg[colon_idx + 1:].strip()
        if key.startswith('"') and key.endswith('"'):
            key = key[1:-1]
        else:
            key = key.strip()
        if val.startswith('"') and val.endswith('"'):
            result[key] = val[1:-1]
        elif val == "true":
            result[key] = True
        elif val == "false":
            result[key] = False
        elif val == "null":
            result[key] = None
        else:
            try:
                result[key] = int(val)
            except ValueError:
                try:
                    result[key] = float(val)
                except ValueError:
                    result[key] = val
    return result


def to_json_val(v):
    if v is None:
        return "null"
    if isinstance(v, bool):
        return "true" if v else "false"
    if isinstance(v, (int, float)):
        return str(v)
    if isinstance(v, dict):
        return "{" + ", ".join('"' + str(k) + '": ' + to_json_val(val) for k, val in v.items()) + "}"
    if isinstance(v, list):
        return "[" + ", ".join(to_json_val(x) for x in v) + "]"
    return "\\"" + str(v) + "\\""


def write_json(path, data):
    lines = []
    lines.append("{")
    items = list(data.items())
    for i, (k, v) in enumerate(items):
        lines.append("  \\"" + str(k) + "\\": " + to_json_val(v) + ("," if i < len(items) - 1 else ""))
    lines.append("}")
    with open(path, "w", encoding="utf-8") as f:
        f.write("\\n".join(lines) + "\\n")
"""

    # --------------------------------------------------------------------
    # Builder
    # --------------------------------------------------------------------

    def _build_script(self, command: ReaScriptCommand) -> str:
        """Build the full ReaScript source with per-action dispatch."""
        # Normalize action to list (backward compat: str → [str])
        raw = command.action
        if isinstance(raw, str):
            actions = [raw] if raw else ["calibrate"]
        elif isinstance(raw, list):
            actions = raw if raw else ["calibrate"]
        else:
            actions = ["calibrate"]

        lines: list[str] = []
        lines.append("# -*- coding: utf-8 -*-")
        lines.append("# auto-generated by fl_control ReaScriptGenerator")
        lines.append('__doc__ = "fl_control Phase 2 ReaScript"')
        lines.append("")

        # API check function (adaptive)
        lines.append(self._api_check_src(actions))
        lines.append("")

        # JSON utilities (hand-rolled, no import json)
        lines.append(self.JSON_PARSER_SRC)
        lines.append("")

        # Track-find helper (needed by add_plugins / configure_fx_params)
        needs_find_track = any(a in actions for a in ("add_plugins", "configure_fx_params"))
        if needs_find_track:
            lines.append(self._find_track_src())
            lines.append("")

        # Per-action functions (emitted only if their action is present)
        if "add_plugins" in actions:
            lines.append(self._add_plugins_src())
            lines.append("")
        if "configure_fx_params" in actions:
            lines.append(self._configure_fx_params_src())
            lines.append("")

        # verify_fx, calibrate, render are always emitted when present
        if "verify_fx" in actions:
            lines.append(self._verify_fx_src())
            lines.append("")
        if "calibrate" in actions:
            lines.append(self._calibrate_src())
            lines.append("")
        if "render" in actions:
            lines.append(self._render_src())
            lines.append("")

        # Main block with dispatch loop
        lines.append(self._main_block_src(actions))
        lines.append("")
        lines.append("main()")

        return "\n".join(lines)

    # --------------------------------------------------------------------
    # Per-action generated source blocks
    # --------------------------------------------------------------------

    def _find_track_src(self) -> str:
        return """\
def find_track(name):
    num_tracks = RPR_CountTracks(0)
    for t in range(num_tracks):
        track = RPR_GetTrack(0, t)
        buf = chr(0) * 256
        RPR_GetSetMediaTrackInfo_String(track, "P_NAME", buf, False)
        track_name = buf.rstrip(chr(0)).strip()
        if track_name.lower() == name.lower():
            return track
    return None
"""

    def _add_plugins_src(self) -> str:
        return """\
def add_plugins(cmd):
    plugins = cmd.get("plugins_to_add", [])
    results = []
    for p in plugins:
        track_name = p.get("track_name", "")
        fx_name = p.get("fx_name", "")
        track = find_track(track_name)
        if track is None:
            results.append({
                "fx_name": fx_name,
                "instance_id": -1,
                "track_name": track_name,
                "status": "error: track not found: " + track_name
            })
            continue
        fx_idx = RPR_TrackFX_AddByName(track, fx_name, False, 1)
        if fx_idx >= 0:
            buf = chr(0) * 512
            RPR_TrackFX_GetFXName(track, fx_idx, buf, 512)
            returned_name = buf.rstrip(chr(0)).strip()
            if returned_name.lower() == fx_name.lower():
                results.append({
                    "fx_name": fx_name,
                    "instance_id": fx_idx,
                    "track_name": track_name,
                    "status": "ok"
                })
            else:
                results.append({
                    "fx_name": fx_name,
                    "instance_id": fx_idx,
                    "track_name": track_name,
                    "status": "failed to load " + fx_name + " on track " + track_name
                })
        else:
            results.append({
                "fx_name": fx_name,
                "instance_id": -1,
                "track_name": track_name,
                "status": "failed to load " + fx_name + " on track " + track_name
            })
    return results
"""

    def _configure_fx_params_src(self) -> str:
        return """\
def configure_fx_params(cmd):
    plugins = cmd.get("plugins_to_add", [])
    for p in plugins:
        track_name = p.get("track_name", "")
        track = find_track(track_name)
        if track is None:
            continue
        fx_count = RPR_TrackFX_GetCount(track, False)
        fx_name = p.get("fx_name", "")
        target_idx = -1
        for fi in range(fx_count):
            buf = chr(0) * 512
            RPR_TrackFX_GetFXName(track, fi, buf, 512)
            returned_name = buf.rstrip(chr(0)).strip()
            if returned_name.lower() == fx_name.lower():
                target_idx = fi
                break
        if target_idx < 0:
            continue
        params = p.get("params", {})
        for param_idx_str, value in params.items():
            param_idx = int(param_idx_str)
            RPR_TrackFX_SetParam(track, target_idx, param_idx, value)
"""

    def _verify_fx_src(self) -> str:
        return """\
def verify_fx():
    fx_errors = []
    tracks_verified = 0
    num_tracks = RPR_CountTracks(0)
    for t in range(num_tracks):
        track = RPR_GetTrack(0, t)
        fx_count = RPR_TrackFX_GetCount(track, False)
        for fi in range(fx_count):
            buf = chr(0) * 512
            RPR_TrackFX_GetFXName(track, fi, buf, 512)
            fx_name = buf.rstrip(chr(0)).strip()
            if not fx_name:
                fx_errors.append({
                    "track_index": t,
                    "fx_index": fi,
                    "name": "",
                    "expected": ""
                })
        tracks_verified += 1
    return fx_errors, tracks_verified
"""

    def _calibrate_src(self) -> str:
        return """\
def calibrate(track_cal):
    for cal in track_cal:
        track_idx = cal.get("track_index", 0)
        track = RPR_GetTrack(0, track_idx)
        vol = cal.get("volume", 1.0)
        pan = cal.get("pan", 0.0)
        RPR_SetMediaTrackInfo_Value(track, "D_VOL", vol)
        RPR_SetMediaTrackInfo_Value(track, "D_PAN", pan)
        for send in cal.get("sends", []):
            dest_idx = send.get("dest_track_index", -1)
            level = send.get("level", 0.0)
            if dest_idx >= 0:
                dest_track = RPR_GetTrack(0, dest_idx)
                RPR_CreateTrackSend(track, dest_track)
"""

    def _render_src(self) -> str:
        return """\
def do_render(render_path):
    if render_path:
        RPR_Main_RenderFile(0, render_path)

def measure_loudness(render_path):
    lufs = None
    integrated_lufs = None
    short_term_lufs = None
    if render_path:
        try:
            loudness_str = RPR_CalcMediaSrcLoudness(render_path, True)
            parts = loudness_str.split(",")
            for p in parts:
                p = p.strip().lower()
                if "integrated" in p:
                    try:
                        integrated_lufs = float(p.split(":")[1].split("lufs")[0].strip())
                    except (ValueError, IndexError):
                        pass
                elif "short-term" in p or "short term" in p:
                    try:
                        short_term_lufs = float(p.split(":")[1].split("lufs")[0].strip())
                    except (ValueError, IndexError):
                        pass
            lufs = integrated_lufs
        except Exception:
            pass
    return lufs, integrated_lufs, short_term_lufs
"""

    # --------------------------------------------------------------------
    # API check (adaptive)
    # --------------------------------------------------------------------

    def _api_check_src(self, actions: list[str]) -> str:
        """Generate check_api() that requires FX APIs only when relevant actions present."""
        # Base required APIs (always needed)
        required_lines = [
            '        "Main_openProject",',
            '        "TrackFX_GetCount",',
            '        "TrackFX_GetFXName",',
            '        "SetMediaTrackInfo_Value",',
            '        "CreateTrackSend",',
            '        "Main_RenderFile",',
            '        "CalcMediaSrcLoudness",',
            '        "GetLastError",',
            '        "GetProjectName",',
        ]

        needs_fx = any(a in actions for a in ("add_plugins", "configure_fx_params"))
        if needs_fx:
            required_lines.append('        "TrackFX_AddByName",')
            required_lines.append('        "TrackFX_SetParam",')

        required = "\n".join(required_lines)

        return f"""\
def check_api():
    required = [
{required}
    ]
    missing = []
    for name in required:
        if RPR_GetFunctionMetadata(name) == "":
            missing.append(name)
    return missing
"""

    # --------------------------------------------------------------------
    # Main block (dispatch loop)
    # --------------------------------------------------------------------

    def _main_block_src(self, actions: list[str]) -> str:
        """Generate main() with dispatch loop over actions list."""
        return """\
def main():
    cmd_path = RPR_ResourcePath() + "scripts/fl_control_command.json"
    res_path = RPR_ResourcePath() + "scripts/fl_control_result.json"

    # Read command JSON
    try:
        with open(cmd_path, encoding="utf-8") as f:
            cmd_text = f.read()
        cmd = parse_json(cmd_text)
    except Exception as e:
        write_json(res_path, {"version": 1, "status": "error", "message": "failed to read command: " + str(e)})
        return

    # API availability check
    missing = check_api()
    if missing:
        write_json(res_path, {"version": 1, "status": "error", "message": "missing API: " + ", ".join(missing)})
        return

    rpp_path = cmd.get("rpp_path", "")
    render_path = cmd.get("render_path", "")
    action = cmd.get("action", "calibrate")
    # Normalize action to list
    if isinstance(action, str):
        action_list = [action] if action else ["calibrate"]
    else:
        action_list = action if action else ["calibrate"]
    track_cal = cmd.get("track_calibration", [])

    # 0. Open project
    RPR_Main_openProject(rpp_path)

    # State variables populated by actions
    fx_errors = []
    tracks_verified = 0
    added_plugins = []
    lufs = None
    integrated_lufs = None
    short_term_lufs = None

    # 1. Dispatch loop
    for action_name in action_list:
        if action_name == "add_plugins":
            added_plugins = add_plugins(cmd)
        elif action_name == "configure_fx_params":
            configure_fx_params(cmd)
        elif action_name == "verify_fx":
            fx_errors, tracks_verified = verify_fx()
        elif action_name == "calibrate":
            calibrate(track_cal)
        elif action_name == "render":
            do_render(render_path)
            lufs, integrated_lufs, short_term_lufs = measure_loudness(render_path)

    # 2. Write result JSON
    result = {
        "version": 1,
        "status": "ok",
        "message": "",
        "lufs": lufs,
        "integrated_lufs": integrated_lufs,
        "short_term_lufs": short_term_lufs,
        "fx_errors": fx_errors,
        "tracks_verified": tracks_verified,
        "added_plugins": added_plugins,
    }
    write_json(res_path, result)
"""
