"""Text-to-Speech (TTS) synthesis helpers.

Thin wrapper around iFlytek (科大讯飞) WebSocket TTS API that returns audio bytes.

Public API:
- `synthesize_speech_bytes`: returns WAV or PCM bytes (no disk I/O)
- `generate_speech`/`generate_speech_bytes`: aliases of the bytes variant

Environment variables (or pass explicitly):
- `FLYTEK_APPID`
- `FLYTEK_APIKey`
- `FLYTEK_APISecret`

Default endpoint uses Singapore region (same as the demo):
- `wss://tts-api-sg.xf-yun.com/v2/tts` (override via env `FLYTEK_URL`)

Notes:
- Uses `websocket-client` (imported as `import websocket`)
- Uses logging for library messages; CLI handles user-facing prints
"""

from __future__ import annotations

import base64
import hashlib
import hmac
import json
import logging
import os
import ssl
import time
from dataclasses import dataclass
from datetime import datetime
from pathlib import Path
from time import mktime
from typing import Any, Dict, Optional, Tuple
from urllib.parse import urlencode

import websocket  # provided by websocket-client
from wsgiref.handlers import format_date_time

from ._env import load_env_once


logger = logging.getLogger(__name__)


DEFAULT_ENDPOINT = "wss://tts-api-sg.xf-yun.com/v2/tts"


@dataclass
class _WsAuth:
    app_id: str
    api_key: str
    api_secret: str


def _build_signed_url(endpoint: str, api_key: str, api_secret: str) -> str:
    """Build a signed WebSocket URL for iFlytek TTS.

    Mirrors the signing flow from the demo script.
    """
    # Normalize host + path
    # Expect endpoint like: wss://tts-api-sg.xf-yun.com/v2/tts
    try:
        host = endpoint.split("//", 1)[1].split("/", 1)[0]
        path = "/" + endpoint.split(host, 1)[1].split("/", 1)[1]
    except Exception:
        # Fallback to known defaults
        host = "tts-api-sg.xf-yun.com"
        path = "/v2/tts"

    now = datetime.now()
    date = format_date_time(mktime(now.timetuple()))

    # host + date + request-line signature
    signature_origin = f"host: {host}\n"
    signature_origin += f"date: {date}\n"
    signature_origin += f"GET {path} HTTP/1.1"
    signature_sha = hmac.new(api_secret.encode("utf-8"), signature_origin.encode("utf-8"), digestmod=hashlib.sha256).digest()
    signature_b64 = base64.b64encode(signature_sha).decode("utf-8")

    authorization_origin = (
        f"api_key=\"{api_key}\", algorithm=\"hmac-sha256\", headers=\"host date request-line\", signature=\"{signature_b64}\""
    )
    authorization = base64.b64encode(authorization_origin.encode("utf-8")).decode("utf-8")

    params = {
        "authorization": authorization,
        "date": date,
        "host": host,
    }
    return endpoint + "?" + urlencode(params)


def _resolve_auth(app_id: Optional[str], api_key: Optional[str], api_secret: Optional[str]) -> _WsAuth:
    """Resolve credentials from args or environment."""
    # Load .env if present (repo root or package-local), but never override
    load_env_once()
    # Only support the specified env names
    app_id = app_id or os.environ.get("FLYTEK_APPID")
    api_key = api_key or os.environ.get("FLYTEK_APIKey")
    api_secret = api_secret or os.environ.get("FLYTEK_APISecret")
    missing = [name for name, val in (("FLYTEK_APPID", app_id), ("FLYTEK_APIKey", api_key), ("FLYTEK_APISecret", api_secret)) if not val]
    if missing:
        raise ValueError(
            "Missing iFlytek credentials: "
            + ", ".join(missing)
            + ". Set environment variables or pass explicitly."
        )
    return _WsAuth(app_id=app_id, api_key=api_key, api_secret=api_secret)


# Disk-output variant removed: only bytes-oriented API is kept.


def synthesize_speech_bytes(
    text: str,
    *,
    voice: Optional[str] = None,
    speed: Optional[int] = None,
    volume: Optional[int] = None,
    pitch: Optional[int] = None,
    sample_rate: int = 16000,
    app_id: Optional[str] = None,
    api_key: Optional[str] = None,
    api_secret: Optional[str] = None,
    endpoint: Optional[str] = None,
    timeout: float = 60.0,
    audio_format: str = "wav",
    provider: str = "algieba",
    model: Optional[str] = None,
) -> bytes:
    """Synthesize speech and return audio bytes (no disk I/O).

    - audio_format: 'wav' (default) or 'pcm'. When 'wav', PCM is wrapped into a WAV container in-memory.
    - provider: 'algieba' (default, Gemini) or 'iflytek'. When 'google'/'gemini'/'algieba', forwards to Google Gemini TTS.
    - model: Optional provider-specific model. For Google, e.g., 'gemini-2.5-pro-tts'.
    - Other parameters have the same semantics as the function arguments above.
    """
    if not text or not isinstance(text, str):
        raise ValueError("text must be a non-empty string")

    provider_key = provider.lower().strip() if isinstance(provider, str) else "algieba"

    # Route to ElevenLabs TTS if requested
    if provider_key in ("elevenlabs", "11labs", "eleven"):
        if (audio_format or "").lower() != "mp3":
            raise ValueError("ElevenLabs provider currently only supports audio_format='mp3'")
        from .tts_elevenlabs import synthesize_speech_elevenlabs_bytes

        return synthesize_speech_elevenlabs_bytes(
            text=text,
            voice_id=voice,
            model_id=model,
            api_key=None,
        )

    # Route to Google Gemini TTS if requested
    if provider_key in ("google", "gemini", "algieba"):
        try:
            from .tts_google import synthesize_speech_google_bytes, DEFAULT_GOOGLE_TTS_MODEL
        except Exception as e:
            raise RuntimeError(f"Google TTS support not available: {e}")
        # Default Gemini voice: Algieba (male), unless user overrides or sets env
        if not voice:
            voice = os.environ.get("GEMINI_TTS_VOICE") or "Algieba"
        return synthesize_speech_google_bytes(
            text=text,
            voice=voice,
            sample_rate=sample_rate,
            model=(model or DEFAULT_GOOGLE_TTS_MODEL),
            audio_format=audio_format,
            timeout=timeout,
        )

    auth = _resolve_auth(app_id, api_key, api_secret)
    # Resolve endpoint from env or argument
    endpoint = endpoint or os.environ.get("FLYTEK_URL") or DEFAULT_ENDPOINT
    ws_url = _build_signed_url(endpoint, auth.api_key, auth.api_secret)

    business: Dict[str, Any] = {
        "aue": "raw",
        "auf": f"audio/L16;rate={int(sample_rate)}",
        "tte": "utf8",
        "vcn": voice or "x_xiaoyang_story",
    }
    if speed is not None:
        business["speed"] = speed
    if volume is not None:
        business["volume"] = volume
    if pitch is not None:
        business["pitch"] = pitch

    payload = {
        "common": {"app_id": auth.app_id},
        "business": business,
        "data": {
            "status": 2,
            "text": str(base64.b64encode(text.encode("utf-8")), "utf-8"),
        },
    }

    state: Dict[str, Any] = {
        "audio_chunks": [],
        "sid": None,
        "code": 0,
        "closed": False,
        "error": None,
    }

    def _on_message(ws, message: str) -> None:  # type: ignore[no-redef]
        try:
            msg = json.loads(message)
            code = int(msg.get("code", 0))
            data = msg.get("data") or {}
            audio_b64 = data.get("audio")
            if audio_b64:
                state["audio_chunks"].append(base64.b64decode(audio_b64))
            state["code"] = code
            if code != 0:
                # propagate error via state; we'll raise after close
                state["error"] = msg.get("message") or f"TTS error code={code}"
            if data.get("status") == 2:
                try:
                    ws.close()
                finally:
                    state["closed"] = True
        except Exception as e:
            state["error"] = e

    def _on_error(ws, error):  # type: ignore[no-redef]
        state["error"] = error

    def _on_close(ws, close_status_code, close_msg):  # type: ignore[no-redef]
        state["closed"] = True

    def _on_open(ws):  # type: ignore[no-redef]
        ws.send(json.dumps(payload))

    ws_app = websocket.WebSocketApp(
        ws_url,
        on_message=_on_message,
        on_error=_on_error,
        on_close=_on_close,
    )
    ws_app.on_open = _on_open
    sslopt = {"cert_reqs": ssl.CERT_NONE}
    ws_app.run_forever(sslopt=sslopt)

    start = time.time()
    while not state["closed"] and (time.time() - start) < timeout:
        time.sleep(0.05)

    if not state["closed"]:
        raise RuntimeError("TTS synthesis timeout")
    if state.get("error") is not None and state.get("code", 0) != 0:
        raise RuntimeError(str(state["error"]))

    pcm = b"".join(state["audio_chunks"])  # type: ignore[arg-type]
    if audio_format.lower() == "pcm":
        return pcm

    # Wrap as WAV in-memory
    import wave, io
    buf = io.BytesIO()
    with wave.open(buf, "wb") as wf:
        wf.setnchannels(1)
        wf.setsampwidth(2)
        wf.setframerate(sample_rate)
        wf.writeframes(pcm)
    return buf.getvalue()


def generate_speech(*args, **kwargs):
    """Alias returning WAV bytes by default for SDK usage."""
    return synthesize_speech_bytes(*args, **kwargs)


def generate_speech_bytes(*args, **kwargs):
    """Alias of `synthesize_speech_bytes` (bytes-oriented SDK)."""
    return synthesize_speech_bytes(*args, **kwargs)


__all__ = [
    "synthesize_speech_bytes",    # bytes-only variant
    "generate_speech",            # alias -> bytes
    "generate_speech_bytes",      # alias -> bytes
]
