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

import argparse
import http.client
import json
import os
import subprocess
import sys
import time
import urllib.error
import urllib.request


DEFAULT_URL = os.environ.get("KSAY_URL", "http://127.0.0.1:7332")
LAUNCHD_LABEL = os.environ.get("KSAY_LAUNCHD_LABEL", "com.local.ksay-kokoro")


def read_stdin_if_needed(text_parts: list[str]) -> str:
    if text_parts:
        return " ".join(text_parts)
    if not sys.stdin.isatty():
        return sys.stdin.read()
    return ""


def request_json(method: str, path: str, payload: dict | None = None) -> dict:
    data = None
    headers = {}
    if payload is not None:
        data = json.dumps(payload).encode("utf-8")
        headers["content-type"] = "application/json"
    req = urllib.request.Request(
        f"{DEFAULT_URL}{path}",
        data=data,
        headers=headers,
        method=method,
    )
    try:
        with urllib.request.urlopen(req, timeout=120) as res:
            return json.loads(res.read().decode("utf-8"))
    except urllib.error.HTTPError as exc:
        body = exc.read().decode("utf-8", errors="replace")
        try:
            return json.loads(body)
        except json.JSONDecodeError:
            raise RuntimeError(f"ksay server returned HTTP {exc.code}: {body}") from exc


def try_wake_service() -> None:
    domain = f"gui/{os.getuid()}"
    subprocess.run(
        ["launchctl", "kickstart", "-k", f"{domain}/{LAUNCHD_LABEL}"],
        stdout=subprocess.DEVNULL,
        stderr=subprocess.DEVNULL,
        check=False,
    )


def post_say(payload: dict) -> dict:
    try:
        return request_json("POST", "/say", payload)
    except (
        ConnectionError,
        ConnectionResetError,
        http.client.RemoteDisconnected,
        urllib.error.URLError,
    ):
        try_wake_service()
        time.sleep(1.0)
        return request_json("POST", "/say", payload)


def play_audio(path: str) -> None:
    subprocess.run(["/usr/bin/afplay", path], check=True)


def main() -> int:
    parser = argparse.ArgumentParser(
        prog="ksay",
        description="Fast warm Kokoro TTS, intended as a neural replacement for macOS say.",
    )
    parser.add_argument("text", nargs="*", help="Text to speak. Reads stdin when omitted.")
    parser.add_argument("-v", "--voice", default=os.environ.get("KSAY_VOICE", "af_heart"))
    parser.add_argument("-r", "--rate", type=float, default=None, help="Compatibility alias; maps words/minute-ish values to speed.")
    parser.add_argument("--speed", type=float, default=None, help="Kokoro speed multiplier.")
    parser.add_argument("--lang-code", default=os.environ.get("KSAY_LANG_CODE", "a"))
    parser.add_argument("-o", "--output", help="Write WAV to this path.")
    parser.add_argument("--no-play", action="store_true", help="Generate the file without playing it.")
    parser.add_argument("--json", action="store_true", help="Print the server response as JSON.")
    parser.add_argument("--status", action="store_true", help="Show warm server health.")
    parser.add_argument("--start", action="store_true", help="Kick the launchd service and wait for health.")
    args = parser.parse_args()

    if args.status:
        print(json.dumps(request_json("GET", "/health"), indent=2))
        return 0

    if args.start:
        try_wake_service()
        for _ in range(60):
            try:
                print(json.dumps(request_json("GET", "/health"), indent=2))
                return 0
            except Exception:
                time.sleep(1)
        print("ksay service did not become healthy within 60s.", file=sys.stderr)
        return 1

    text = read_stdin_if_needed(args.text).strip()
    if not text:
        parser.error("text is required, or pipe text on stdin")

    speed = args.speed
    if speed is None:
        speed = 1.0
        if args.rate:
            speed = max(0.5, min(2.0, args.rate / 180.0))

    result = post_say(
        {
            "text": text,
            "voice": args.voice,
            "speed": speed,
            "langCode": args.lang_code,
            "output": args.output,
        }
    )
    if not result.get("ok"):
        print(result.get("error", "ksay failed"), file=sys.stderr)
        return 1

    if args.json:
        print(json.dumps(result, indent=2))
    else:
        print(result["filePath"])

    if not args.no_play:
        play_audio(result["filePath"])

    return 0


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