#!/usr/bin/env python3
"""
概要:
    非常駐型Irodori-TTSのFlask API用Windows/Linuxコマンドラインクライアントです。

詳細説明:
    環境変数 IRODORI_TTS_API またはコマンドライン引数で指定されたサーバーに対して
    HTTPリクエストを送信し、音声合成またはサーバー情報の取得を行います。

関連リンク:
    irodori_tts_api_usage
"""

from __future__ import annotations

import argparse
import json
import os
import sys
import urllib.error
import urllib.request
from pathlib import Path


DEFAULT_SERVER = os.environ.get("IRODORI_TTS_API", "http://192.168.27.18:8002")


def parser() -> argparse.ArgumentParser:
    """
    概要:
        コマンドライン引数パーサーを構築します。

    詳細説明:
        テキスト入力、サーバー設定、出力パス、音声合成の各種パラメータおよび
        サーバー状態確認用の引数を定義します。

    戻り値:
        :returns: コマンドライン引数を解析するためのパーサーオブジェクト。
        :rtype: argparse.ArgumentParser
    """
    argparser = argparse.ArgumentParser(description="Irodori-TTS non-resident API client")
    text_group = argparser.add_mutually_exclusive_group()
    text_group.add_argument("--text", help="text to synthesize")
    text_group.add_argument("--text-file", type=Path, help="UTF-8 text file to synthesize")
    argparser.add_argument("--server", default=DEFAULT_SERVER, help=f"API root (default: {DEFAULT_SERVER})")
    argparser.add_argument("--output", type=Path, help="output WAV path")
    argparser.add_argument("--voice", default="none", help="registered server-side voice ID, or 'none'")
    argparser.add_argument("--caption", help="Irodori Voice Design caption")
    argparser.add_argument("--num-steps", type=int, help="sampling steps")
    argparser.add_argument("--duration-scale", type=float, help="duration multiplier")
    argparser.add_argument("--sway", action="store_true", help="use sway sampling schedule")
    argparser.add_argument("--sway-coeff", type=float, default=-1.0, help="sway coefficient (default: -1.0)")
    argparser.add_argument("--timeout", type=float, default=1800, help="HTTP timeout in seconds (default: 1800)")
    argparser.add_argument("--list-voices", action="store_true", help="list registered server-side voices")
    argparser.add_argument("--health", action="store_true", help="show server health and exit")
    return argparser


def request_json(url: str, timeout: float) -> dict:
    """
    概要:
        指定したURLに対してGETリクエストを送信し、JSONレスポンスを辞書として取得します。

    詳細説明:
        urllib.requestを使用して同期的にリクエストを行い、レスポンスのUTF-8文字列をJSONとして解析します。

    引数:
        :param url: リクエスト先のURL文字列。
        :type url: str
        :param timeout: タイムアウト時間(秒)。
        :type timeout: float

    戻り値:
        :returns: 解析されたJSONデータ(辞書)。
        :rtype: dict
    """
    with urllib.request.urlopen(url, timeout=timeout) as response:
        return json.loads(response.read().decode("utf-8"))


def main() -> int:
    """
    概要:
        コマンドライン引数を解析し、音声合成またはサーバー状態取得のAPIリクエストを実行します。

    詳細説明:
        healthやlist_voicesが指定された場合はJSONを取得してコンソールに表示します。
        音声合成の場合は、指定されたテキストと各種パラメータからJSONペイロードを構築し、
        POSTリクエストを送信して応答をWAV音声ファイルとして保存します。

    戻り値:
        :returns: 実行結果を表す終了コード。成功時は0、エラー時は1。
        :rtype: int
    """
    args = parser().parse_args()
    root = args.server.rstrip("/")
    try:
        if args.health:
            print(json.dumps(request_json(f"{root}/health", args.timeout), ensure_ascii=False, indent=2))
            return 0
        if args.list_voices:
            listing = request_json(f"{root}/v1/audio/voices", args.timeout)
            for item in listing.get("data", []):
                print(item.get("id", ""))
            return 0
        if args.text is not None:
            text = args.text
        elif args.text_file is not None:
            text = args.text_file.read_text(encoding="utf-8")
        else:
            parser().error("one of --text or --text-file is required unless --health/--list-voices is used")
        if args.output is None:
            parser().error("--output is required for synthesis")

        irodori: dict[str, object] = {}
        if args.caption:
            irodori["caption"] = args.caption
        if args.num_steps is not None:
            irodori["num_steps"] = args.num_steps
        if args.duration_scale is not None:
            irodori["duration_scale"] = args.duration_scale
        if args.sway:
            irodori["t_schedule_mode"] = "sway"
            irodori["sway_coeff"] = args.sway_coeff
        payload = json.dumps({"model": "irodori-tts", "input": text, "voice": args.voice, "response_format": "wav", "irodori": irodori}, ensure_ascii=False).encode("utf-8")
        request_object = urllib.request.Request(f"{root}/v1/audio/speech", data=payload, headers={"Content-Type": "application/json", "Accept": "audio/wav"}, method="POST")
        with urllib.request.urlopen(request_object, timeout=args.timeout) as response:
            audio = response.read()
        if not audio:
            raise RuntimeError("server returned an empty audio response")
        args.output.parent.mkdir(parents=True, exist_ok=True)
        args.output.write_bytes(audio)
        print(f"Saved: {args.output} ({len(audio):,} bytes)")
        return 0
    except urllib.error.HTTPError as exc:
        detail = exc.read().decode("utf-8", errors="replace")
        print(f"HTTP {exc.code}: {detail}", file=sys.stderr)
        return 1
    except (OSError, RuntimeError, urllib.error.URLError, json.JSONDecodeError) as exc:
        print(f"Error: {exc}", file=sys.stderr)
        return 1


if __name__ == "__main__":
    raise SystemExit(main())