#!/usr/bin/env python3
from __future__ import annotations

import argparse
import json
import sys
from pathlib import Path

import requests


DEFAULT_BASE_URL = "http://localhost:7860"


def die(message: str, code: int = 1) -> None:
    print(f"error: {message}", file=sys.stderr)
    raise SystemExit(code)


def clean_base(url: str) -> str:
    return str(url or DEFAULT_BASE_URL).rstrip("/")


def request(args, method: str, path: str, **kwargs):
    url = clean_base(args.base_url) + path
    try:
        resp = requests.request(method, url, timeout=args.timeout, **kwargs)
    except requests.RequestException as exc:
        die(f"{method} {url} failed: {exc}")
    if not resp.ok:
        detail = resp.text
        try:
            detail = resp.json().get("detail", detail)
        except Exception:
            pass
        die(f"{method} {url} returned HTTP {resp.status_code}: {detail}")
    return resp


def get_json(args, path: str):
    return request(args, "GET", path).json()


def post_json(args, path: str, payload: dict):
    return request(args, "POST", path, json=payload).json()


def print_json(data) -> None:
    print(json.dumps(data, indent=2, ensure_ascii=False))


def print_table(rows: list[dict], columns: list[tuple[str, str]]) -> None:
    if not rows:
        print("(none)")
        return
    widths = {}
    for key, title in columns:
        widths[key] = max(len(title), *(len(str(r.get(key, ""))) for r in rows))
    print("  ".join(title.ljust(widths[key]) for key, title in columns))
    print("  ".join("-" * widths[key] for key, _title in columns))
    for row in rows:
        print("  ".join(str(row.get(key, "")).ljust(widths[key]) for key, _title in columns))


def cmd_settings(args) -> None:
    if args.settings_cmd == "show":
      print_json(get_json(args, "/api/settings"))
      return

    current = get_json(args, "/api/settings")
    updates = {
        "whisper_url": args.whisper_url,
        "whisper_api_key": args.whisper_key,
        "tts_url": args.tts_url,
        "tts_api_key": args.tts_key,
        "tts_backend": args.tts_backend,
        "voice_design_url": args.voice_design_url,
        "voice_design_api_key": args.voice_design_key,
        "nvidia_router_url": args.nvidia_router_url,
        "nvidia_tts_url": args.nvidia_tts_url,
        "nvidia_asr_url": args.nvidia_asr_url,
        "nvidia_clone_url": args.nvidia_clone_url,
        "voices_scan_dir": args.voices_scan_dir,
        "output_dir": args.output_dir,
    }
    current.update({k: v for k, v in updates.items() if v is not None})
    print_json(post_json(args, "/api/settings", current))


def cmd_voices(args) -> None:
    if args.voices_cmd == "list":
        voices = get_json(args, "/api/voices")
        if not args.all:
            voices = [v for v in voices if v.get("enabled") is not False]
        if args.json:
            print_json(voices)
            return
        rows = [{
            "id": v.get("id", ""),
            "lang": v.get("language") or v.get("flag") or "",
            "gender": v.get("gender", ""),
            "type": v.get("file_type", ""),
            "duration": f"{float(v.get('duration') or 0):.1f}",
            "db": (v.get("loudness") or {}).get("dbfs", ""),
            "bench": (v.get("benchmark") or {}).get("elapsed_sec", ""),
            "ttfa": (v.get("benchmark") or {}).get("ttfa_ms", ""),
            "speed": (v.get("benchmark") or {}).get("speed", ""),
            "active": "yes" if v.get("enabled") is not False else "no",
        } for v in voices]
        print_table(rows, [("id", "Voice"), ("lang", "Lang"), ("gender", "Sex"), ("type", "Type"), ("duration", "Sec"), ("db", "dB"), ("bench", "Bench"), ("ttfa", "TTFA"), ("speed", "Speed"), ("active", "Active")])
    elif args.voices_cmd in {"enable", "disable"}:
        print_json(post_json(args, "/api/voice/meta", {"voice_id": args.voice, "enabled": args.voices_cmd == "enable"}))
    elif args.voices_cmd == "delete":
        if not args.yes:
            die("delete needs --yes")
        print_json(request(args, "DELETE", f"/api/voice/{args.voice}").json())
    elif args.voices_cmd == "rename":
        print_json(post_json(args, "/api/voice/rename", {"voice_id": args.voice, "new_id": args.new_id}))
    elif args.voices_cmd == "normalize":
        payload = {"voice_id": args.voice}
        if args.target_dbfs is not None:
            payload["target_dbfs"] = args.target_dbfs
        print_json(post_json(args, "/api/voice/normalize", payload))
    elif args.voices_cmd == "normalize-active":
        print_json(post_json(args, "/api/voices/normalize-active", {}))
    elif args.voices_cmd == "calculate-db":
        print_json(post_json(args, "/api/voices/calculate-db", {}))
    elif args.voices_cmd == "benchmark":
        payload = {
            "active_only": not args.all,
            "limit": args.limit,
            "text": args.text,
        }
        if args.voice:
            payload["voice_id"] = args.voice
        print_json(post_json(args, "/api/voices/benchmark", payload))
    elif args.voices_cmd == "sync":
        print_json(post_json(args, "/api/voices/sync-folders", {}))
    elif args.voices_cmd == "restart-tts":
        print_json(post_json(args, "/api/tts/restart", {}))


def load_routes(args) -> list[dict]:
    return get_json(args, "/api/tts-routes").get("routes", [])


def save_routes(args, routes: list[dict]) -> None:
    print_json(post_json(args, "/api/tts-routes", {"routes": routes}))


def cmd_routes(args) -> None:
    routes = load_routes(args)
    if args.routes_cmd == "list":
        if args.json:
            print_json(routes)
            return
        rows = []
        for i, r in enumerate(routes):
            rows.append({
                "idx": i,
                "on": "yes" if r.get("enabled", True) else "no",
                "app": r.get("app", "*"),
                "input": r.get("input_voice", "*"),
                "lang": r.get("language", "*"),
                "output": r.get("output_voice", ""),
                "before": r.get("before_sound", ""),
                "after": r.get("after_sound", ""),
            })
        print_table(rows, [("idx", "#"), ("on", "On"), ("app", "App"), ("input", "Input"), ("lang", "Lang"), ("output", "Output"), ("before", "Before"), ("after", "After")])
    elif args.routes_cmd == "add":
        routes.append({
            "enabled": not args.disabled,
            "app": args.app,
            "input_voice": args.input_voice,
            "language": args.language.upper(),
            "output_voice": args.output_voice,
            "before_sound": args.before_sound or "",
            "after_sound": args.after_sound or "",
        })
        save_routes(args, routes)
    elif args.routes_cmd == "delete":
        if args.index < 0 or args.index >= len(routes):
            die(f"route index {args.index} out of range")
        del routes[args.index]
        save_routes(args, routes)
    elif args.routes_cmd == "clear":
        if not args.yes:
            die("clear needs --yes")
        save_routes(args, [])
    elif args.routes_cmd == "openwebui-defaults":
        if args.en:
            routes.append({"enabled": True, "app": "Open WebUI", "input_voice": "default", "language": "EN", "output_voice": args.en})
        if args.de:
            routes.append({"enabled": True, "app": "Open WebUI", "input_voice": "default", "language": "DE", "output_voice": args.de})
        if not args.en and not args.de:
            die("provide --en and/or --de output voice")
        save_routes(args, routes)


def cmd_tts(args) -> None:
    if args.tts_cmd == "voices":
        print_json(get_json(args, "/v1/audio/voices"))
        return
    payload = {
        "model": args.model,
        "voice": args.voice,
        "input": args.text,
        "response_format": args.format,
    }
    resp = request(args, "POST", "/v1/audio/speech", json=payload)
    out = Path(args.out)
    out.write_bytes(resp.content)
    print(f"wrote {out}")
    routed = resp.headers.get("x-tts-voice-routed")
    requested = resp.headers.get("x-tts-voice-requested")
    if routed and requested and routed != requested:
        print(f"routed {requested} -> {routed} ({resp.headers.get('x-tts-route-language', '')})")


def cmd_create_voice(args) -> None:
    path = Path(args.file)
    if not path.exists():
        die(f"file not found: {path}")
    with path.open("rb") as fh:
        upload = request(args, "POST", "/api/upload", files={"file": (path.name, fh)}).json()
    start = args.start
    end = args.end if args.end is not None else upload.get("duration", 0)
    processed = post_json(args, "/api/process", {"id": upload["id"], "start": start, "end": end})
    saved = post_json(args, "/api/save", {
        "id": processed["id"],
        "voice_id": args.voice_id,
        "transcript": args.transcript or "",
    })
    print_json({"upload": upload, "processed": processed, "saved": saved})


def cmd_presets(args) -> None:
    presets = get_json(args, "/api/voice-design-presets")
    if args.presets_cmd == "list":
        print_json(presets)
    elif args.presets_cmd == "delete":
        presets.pop(args.name, None)
        print_json(post_json(args, "/api/voice-design-presets", presets))
    elif args.presets_cmd == "save":
        presets[args.name] = {
            "description": args.description,
            "language": args.language,
            "gender": args.gender,
            "sample_text": args.sample_text,
        }
        print_json(post_json(args, "/api/voice-design-presets", presets))


def build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(
        prog="ttsvc",
        description="Command line control for TTS Voice Creator - Clone and Design.",
    )
    parser.add_argument("--base-url", default=DEFAULT_BASE_URL, help=f"Creator app URL, default: {DEFAULT_BASE_URL}")
    parser.add_argument("--timeout", type=float, default=120, help="HTTP timeout in seconds")
    sub = parser.add_subparsers(dest="cmd", required=True)

    settings = sub.add_parser("settings", help="Show or update app settings")
    settings_sub = settings.add_subparsers(dest="settings_cmd", required=True)
    settings_sub.add_parser("show", help="Print current settings").set_defaults(func=cmd_settings)
    settings_set = settings_sub.add_parser("set", help="Update selected settings")
    settings_set.add_argument("--whisper-url")
    settings_set.add_argument("--whisper-key")
    settings_set.add_argument("--tts-url")
    settings_set.add_argument("--tts-key")
    settings_set.add_argument("--tts-backend", choices=["openai", "localai", "pocket", "nvidia_magpie"])
    settings_set.add_argument("--voice-design-url")
    settings_set.add_argument("--voice-design-key")
    settings_set.add_argument("--nvidia-router-url")
    settings_set.add_argument("--nvidia-tts-url")
    settings_set.add_argument("--nvidia-asr-url")
    settings_set.add_argument("--nvidia-clone-url")
    settings_set.add_argument("--voices-scan-dir")
    settings_set.add_argument("--output-dir")
    settings_set.set_defaults(func=cmd_settings)

    voices = sub.add_parser("voices", help="Manage cloned voices")
    voices_sub = voices.add_subparsers(dest="voices_cmd", required=True)
    voices_list = voices_sub.add_parser("list", help="List voices")
    voices_list.add_argument("--all", action="store_true", help="Include disabled voices")
    voices_list.add_argument("--json", action="store_true", help="Print raw JSON")
    voices_list.set_defaults(func=cmd_voices)
    for name in ("enable", "disable", "delete", "normalize"):
        p = voices_sub.add_parser(name, help=f"{name.title()} a voice")
        p.add_argument("voice")
        if name == "delete":
            p.add_argument("--yes", action="store_true", help="Confirm deletion")
        if name == "normalize":
            p.add_argument("--target-dbfs", type=float)
        p.set_defaults(func=cmd_voices)
    rename = voices_sub.add_parser("rename", help="Rename a voice package")
    rename.add_argument("voice")
    rename.add_argument("new_id")
    rename.set_defaults(func=cmd_voices)
    benchmark = voices_sub.add_parser("benchmark", help="Benchmark voice synthesis speed")
    benchmark.add_argument("--all", action="store_true", help="Include disabled voices")
    benchmark.add_argument("--limit", type=int, default=0, help="Stop after N voices")
    benchmark.add_argument("--voice", default="", help="Benchmark one voice ID")
    benchmark.add_argument("--text", default="", help="Benchmark sentence")
    benchmark.set_defaults(func=cmd_voices)
    for name in ("normalize-active", "calculate-db", "sync", "restart-tts"):
        voices_sub.add_parser(name, help=name.replace("-", " ").title()).set_defaults(func=cmd_voices)

    routes = sub.add_parser("routes", help="Manage /v1 TTS routing rules")
    routes_sub = routes.add_subparsers(dest="routes_cmd", required=True)
    routes_list = routes_sub.add_parser("list", help="List routing rules")
    routes_list.add_argument("--json", action="store_true")
    routes_list.set_defaults(func=cmd_routes)
    routes_add = routes_sub.add_parser("add", help="Add one route")
    routes_add.add_argument("--app", default="Open WebUI")
    routes_add.add_argument("--input-voice", default="default")
    routes_add.add_argument("--language", default="*", help="*, AUTO, EN, DE, FR, ES, IT, PT, NL, PL")
    routes_add.add_argument("--output-voice", required=True)
    routes_add.add_argument("--before-sound", help="Audio path inside voices dir to prepend, e.g. sounds/start.wav")
    routes_add.add_argument("--after-sound", help="Audio path inside voices dir to append, e.g. sounds/end.wav")
    routes_add.add_argument("--disabled", action="store_true")
    routes_add.set_defaults(func=cmd_routes)
    routes_delete = routes_sub.add_parser("delete", help="Delete route by index")
    routes_delete.add_argument("index", type=int)
    routes_delete.set_defaults(func=cmd_routes)
    routes_clear = routes_sub.add_parser("clear", help="Delete all routes")
    routes_clear.add_argument("--yes", action="store_true")
    routes_clear.set_defaults(func=cmd_routes)
    routes_examples = routes_sub.add_parser("openwebui-defaults", help="Add default EN/DE Open WebUI routes")
    routes_examples.add_argument("--en")
    routes_examples.add_argument("--de")
    routes_examples.set_defaults(func=cmd_routes)

    tts = sub.add_parser("tts", help="Use the OpenAI-compatible TTS proxy")
    tts_sub = tts.add_subparsers(dest="tts_cmd", required=True)
    tts_sub.add_parser("voices", help="List /v1 voices and route aliases").set_defaults(func=cmd_tts)
    speak = tts_sub.add_parser("speak", help="Generate speech through /v1/audio/speech")
    speak.add_argument("--voice", required=True)
    speak.add_argument("--text", required=True)
    speak.add_argument("--out", default="speech.wav")
    speak.add_argument("--model", default="tts-1")
    speak.add_argument("--format", default="wav")
    speak.set_defaults(func=cmd_tts)

    create = sub.add_parser("create-voice", help="Upload, trim, and save a voice from an audio/video file")
    create.add_argument("--file", required=True)
    create.add_argument("--voice-id", required=True)
    create.add_argument("--transcript", default="")
    create.add_argument("--start", type=float, default=0)
    create.add_argument("--end", type=float)
    create.set_defaults(func=cmd_create_voice)

    presets = sub.add_parser("presets", help="Manage Voice Design presets")
    presets_sub = presets.add_subparsers(dest="presets_cmd", required=True)
    presets_sub.add_parser("list", help="List presets").set_defaults(func=cmd_presets)
    preset_save = presets_sub.add_parser("save", help="Create or replace a preset")
    preset_save.add_argument("name")
    preset_save.add_argument("--description", required=True)
    preset_save.add_argument("--language", default="Auto")
    preset_save.add_argument("--gender", default="N")
    preset_save.add_argument("--sample-text", default="")
    preset_save.set_defaults(func=cmd_presets)
    preset_delete = presets_sub.add_parser("delete", help="Delete a preset")
    preset_delete.add_argument("name")
    preset_delete.set_defaults(func=cmd_presets)
    return parser


def main(argv: list[str] | None = None) -> None:
    parser = build_parser()
    args = parser.parse_args(argv)
    args.func(args)


if __name__ == "__main__":
    main()
