#!/usr/bin/env python3
"""Dependency-free client for the hosted Eapil-Voice API."""

from __future__ import annotations

import argparse
import json
import mimetypes
import os
import sys
import uuid
from pathlib import Path
from urllib.error import HTTPError, URLError
from urllib.parse import urljoin
from urllib.request import Request, urlopen


DEFAULT_BASE_URL = "https://ades.playones.com"
KEY_HELP = "missing API key: set OPENAI_API_KEY, pass --token directly, or get a key from https://kinfo.playones.com"


def env_base_url() -> str:
    return os.getenv("QWEN3_VOICE_API_BASE_URL", DEFAULT_BASE_URL).rstrip("/")


def env_token() -> str | None:
    return os.getenv("OPENAI_API_KEY")


def add_common_args(parser: argparse.ArgumentParser) -> None:
    parser.add_argument("--base-url", default=env_base_url(), help="API base URL")
    parser.add_argument("--token", default=env_token(), help="Bearer API key; defaults to OPENAI_API_KEY")


def clean_payload(values: dict[str, object | None]) -> dict[str, object]:
    return {key: value for key, value in values.items() if value is not None}


def bool_value(raw: str | bool | None) -> bool | None:
    if raw is None or isinstance(raw, bool):
        return raw
    value = raw.strip().lower()
    if value in {"1", "true", "yes", "on"}:
        return True
    if value in {"0", "false", "no", "off"}:
        return False
    raise argparse.ArgumentTypeError(f"invalid boolean value: {raw}")


def bearer_token(token: str | None) -> str:
    if not token:
        raise SystemExit(KEY_HELP)
    token = token.strip()
    if token.lower().startswith("bearer "):
        token = token[7:].strip()
    if not token:
        raise SystemExit(KEY_HELP)
    return token


def request_headers(token: str | None, extra: dict[str, str] | None = None) -> dict[str, str]:
    headers = dict(extra or {})
    headers["Authorization"] = f"Bearer {bearer_token(token)}"
    return headers


def build_url(base_url: str, path: str) -> str:
    return urljoin(base_url.rstrip("/") + "/", path.lstrip("/"))


def http_request(
    *,
    method: str,
    base_url: str,
    path: str,
    token: str | None,
    body: bytes | None = None,
    headers: dict[str, str] | None = None,
) -> bytes:
    request = Request(
        build_url(base_url, path),
        data=body,
        method=method,
        headers=request_headers(token, headers),
    )
    try:
        with urlopen(request) as response:
            return response.read()
    except HTTPError as exc:
        detail = exc.read().decode("utf-8", errors="replace")
        raise SystemExit(f"HTTP {exc.code} {exc.reason}: {detail}") from exc
    except URLError as exc:
        raise SystemExit(f"request failed: {exc.reason}") from exc


def post_json(args: argparse.Namespace, path: str, payload: dict[str, object]) -> bytes:
    body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
    return http_request(
        method="POST",
        base_url=args.base_url,
        path=path,
        token=args.token,
        body=body,
        headers={"Content-Type": "application/json"},
    )


def multipart_body(
    *,
    fields: dict[str, object | None],
    file_field: str | None = None,
    file_path: str | None = None,
) -> tuple[bytes, str]:
    boundary = f"----eapil-voice-{uuid.uuid4().hex}"
    chunks: list[bytes] = []

    def add_line(value: str) -> None:
        chunks.append(value.encode("utf-8"))

    def field_value(value: object) -> str:
        if isinstance(value, bool):
            return "true" if value else "false"
        return str(value)

    for name, value in clean_payload(fields).items():
        add_line(f"--{boundary}\r\n")
        add_line(f'Content-Disposition: form-data; name="{name}"\r\n\r\n')
        add_line(f"{field_value(value)}\r\n")

    if file_field and file_path:
        path = Path(file_path)
        content_type = mimetypes.guess_type(path.name)[0] or "application/octet-stream"
        add_line(f"--{boundary}\r\n")
        add_line(
            f'Content-Disposition: form-data; name="{file_field}"; filename="{path.name}"\r\n'
        )
        add_line(f"Content-Type: {content_type}\r\n\r\n")
        chunks.append(path.read_bytes())
        add_line("\r\n")

    add_line(f"--{boundary}--\r\n")
    return b"".join(chunks), f"multipart/form-data; boundary={boundary}"


def post_multipart(
    args: argparse.Namespace,
    path: str,
    fields: dict[str, object | None],
    file_path: str | None,
) -> bytes:
    body, content_type = multipart_body(fields=fields, file_field="file", file_path=file_path)
    return http_request(
        method="POST",
        base_url=args.base_url,
        path=path,
        token=args.token,
        body=body,
        headers={"Content-Type": content_type},
    )


def write_output(path: str, data: bytes) -> None:
    Path(path).write_bytes(data)
    print(path)


def cmd_health(args: argparse.Namespace) -> None:
    data = http_request(
        method="GET",
        base_url=args.base_url,
        path="/health",
        token=args.token,
    )
    print_json(data)


def cmd_models(args: argparse.Namespace) -> None:
    data = http_request(
        method="GET",
        base_url=args.base_url,
        path="/models",
        token=args.token,
    )
    print_json(data)


def print_json(data: bytes) -> None:
    try:
        parsed = json.loads(data.decode("utf-8"))
    except json.JSONDecodeError:
        sys.stdout.buffer.write(data)
        return
    print(json.dumps(parsed, ensure_ascii=False, indent=2))


def cmd_design(args: argparse.Namespace) -> None:
    payload = clean_payload(
        {
            "text": args.text,
            "instruct": args.instruct,
            "provider": args.provider,
            "language": args.language,
            "temperature": args.temperature,
            "max_tokens": args.max_tokens,
            "top_k": args.top_k,
            "top_p": args.top_p,
            "repetition_penalty": args.repetition_penalty,
            "cfg_value": args.cfg_value,
            "inference_timesteps": args.inference_timesteps,
            "normalize": args.normalize,
            "denoise": args.denoise,
            "retry_badcase": args.retry_badcase,
        }
    )
    write_output(args.out, post_json(args, "/v1/voice/design", payload))


def cmd_synthesize(args: argparse.Namespace) -> None:
    payload = clean_payload(
        {
            "text": args.text,
            "speaker": args.speaker,
            "language": args.language,
            "temperature": args.temperature,
            "max_tokens": args.max_tokens,
            "top_k": args.top_k,
            "top_p": args.top_p,
            "repetition_penalty": args.repetition_penalty,
        }
    )
    write_output(args.out, post_json(args, "/v1/voice/synthesize", payload))


def cmd_clone(args: argparse.Namespace) -> None:
    fields = clean_payload(
        {
            "text": args.text,
            "ref_text": args.ref_text,
            "language": args.language,
            "temperature": args.temperature,
            "max_tokens": args.max_tokens,
            "top_k": args.top_k,
            "top_p": args.top_p,
            "repetition_penalty": args.repetition_penalty,
            "provider": args.provider,
            "instruct": args.instruct,
            "cfg_value": args.cfg_value,
            "inference_timesteps": args.inference_timesteps,
            "normalize": args.normalize,
            "denoise": args.denoise,
            "retry_badcase": args.retry_badcase,
        }
    )
    write_output(args.out, post_multipart(args, "/v1/voice/clone", fields, args.file))


def cmd_enhance(args: argparse.Namespace) -> None:
    fields = {"chunked": args.chunked}
    write_output(args.out, post_multipart(args, "/v1/audio/enhance", fields, args.file))


def cmd_denoise(args: argparse.Namespace) -> None:
    fields = {"chunked": args.chunked}
    write_output(args.out, post_multipart(args, "/v1/audio/denoise", fields, args.file))


def cmd_asr(args: argparse.Namespace) -> None:
    fields = clean_payload(
        {
            "language": args.language,
            "temperature": args.temperature,
            "max_tokens": args.max_tokens,
            "top_k": args.top_k,
            "top_p": args.top_p,
            "repetition_penalty": args.repetition_penalty,
        }
    )
    print_json(post_multipart(args, "/v1/asr/transcribe", fields, args.file))


def add_generation_args(parser: argparse.ArgumentParser) -> None:
    parser.add_argument("--language", default=None)
    parser.add_argument("--temperature", type=float, default=None)
    parser.add_argument("--max-tokens", type=int, default=None)
    parser.add_argument("--top-k", type=int, default=None)
    parser.add_argument("--top-p", type=float, default=None)
    parser.add_argument("--repetition-penalty", type=float, default=None)


def add_voxcpm2_args(parser: argparse.ArgumentParser) -> None:
    parser.add_argument("--cfg-value", type=float, default=None)
    parser.add_argument("--inference-timesteps", type=int, default=None)
    parser.add_argument("--normalize", type=bool_value, default=None)
    parser.add_argument("--denoise", type=bool_value, default=None)
    parser.add_argument("--retry-badcase", type=bool_value, default=None)


def build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(description=__doc__)
    subparsers = parser.add_subparsers(dest="command", required=True)

    health = subparsers.add_parser("health", help="Check service health")
    add_common_args(health)
    health.set_defaults(func=cmd_health)

    models = subparsers.add_parser("models", help="Show configured model status")
    add_common_args(models)
    models.set_defaults(func=cmd_models)

    design = subparsers.add_parser("design", help="Generate voice from text and instruction")
    add_common_args(design)
    design.add_argument("--text", required=True)
    design.add_argument("--instruct", required=True)
    design.add_argument("--provider", choices=["qwen", "voxcpm2"], default=None)
    design.add_argument("--out", default="voice_design.wav")
    add_generation_args(design)
    add_voxcpm2_args(design)
    design.set_defaults(func=cmd_design)

    synthesize = subparsers.add_parser("synthesize", help="Base TTS synthesis")
    add_common_args(synthesize)
    synthesize.add_argument("--text", required=True)
    synthesize.add_argument("--speaker", default=None)
    synthesize.add_argument("--out", default="synthesize.wav")
    add_generation_args(synthesize)
    synthesize.set_defaults(func=cmd_synthesize)

    clone = subparsers.add_parser("clone", help="Clone a voice from reference audio")
    add_common_args(clone)
    clone.add_argument("--file", required=True, help="Reference audio path")
    clone.add_argument("--text", required=True)
    clone.add_argument("--ref-text", default=None)
    clone.add_argument("--provider", choices=["qwen", "voxcpm2"], default=None)
    clone.add_argument("--instruct", default=None)
    clone.add_argument("--out", default="voice_clone.wav")
    add_generation_args(clone)
    add_voxcpm2_args(clone)
    clone.set_defaults(func=cmd_clone)

    enhance = subparsers.add_parser("enhance", help="Enhance speech audio")
    add_common_args(enhance)
    enhance.add_argument("--file", required=True)
    enhance.add_argument("--chunked", type=bool_value, default=None)
    enhance.add_argument("--out", default="enhanced.wav")
    enhance.set_defaults(func=cmd_enhance)

    denoise = subparsers.add_parser("denoise", help="Denoise speech audio")
    add_common_args(denoise)
    denoise.add_argument("--file", required=True)
    denoise.add_argument("--chunked", type=bool_value, default=None)
    denoise.add_argument("--out", default="enhanced.wav")
    denoise.set_defaults(func=cmd_denoise)

    asr = subparsers.add_parser("asr", help="Transcribe audio to JSON")
    add_common_args(asr)
    asr.add_argument("--file", required=True)
    add_generation_args(asr)
    asr.set_defaults(func=cmd_asr)

    return parser


def main() -> None:
    args = build_parser().parse_args()
    args.func(args)


if __name__ == "__main__":
    main()
