"""讯飞录音文件转写(LFASR,raasr.xfyun.cn v2)客户端 —— 整个录音文件转文字。

接口文档: https://www.xfyun.cn/doc/asr/ifasr_new/API.html
与 `asr_xfyun`(IAT 语音听写,实时流式,单会话 60s)是**两个独立服务**:LFASR 专为
已成文件的长录音设计 —— 单文件最长 5 小时 / 500MB,服务端自行解码常见音频格式
(mp3/m4a/wav/amr/ogg/flac/...,无需本地 ffmpeg),自带说话人分离(roleType=1)。
转写是异步订单:`upload` 拿 orderId → 轮询 `get_result` 等 status=4。

凭据:env `XFYUN_APPID`(与 IAT 共用)+ `XFYUN_LFASR_SECRET_KEY`(LFASR 服务独立的
SecretKey,讯飞控制台「语音转写」页查看 —— **不是** IAT 的 APIKey/APISecret)。
签名:signa = Base64(HmacSHA1(key=secretKey, msg=MD5hex(appid+ts)))。

全 sync(httpx):调用方是 tool.execute,本就跑在 worker 线程里。
"""
from __future__ import annotations

import base64
import hashlib
import hmac
import json
import os
import time
from typing import Callable, Optional

import httpx

_API_BASE = "https://raasr.xfyun.cn/v2/api"
_POLL_INTERVAL_S = 10.0   # getResult 有频控,10s 一查绰绰有余
_POLL_ERR_TOLERANCE = 5   # 轮询期连续网络错误容忍次数(订单在讯飞侧,不能因抖动丢单)

# orderInfo.failType → 可读提示(文档错误码页;没列到的走通用兜底)
_FAIL_HINTS = {
    1: "音频上传失败",
    2: "音频转码失败(格式可能不支持,建议转成 mp3/wav 再传)",
    3: "音频识别失败",
    4: "音频时长超限(最长 5 小时)",
    5: "音频文件校验失败(文件损坏或为空)",
    6: "未检出有效语音(静音 / 纯音乐?)",
}


class LfasrError(RuntimeError):
    """转写失败(讯飞返回错误 / 连接异常),code 为讯飞错误码字符串(连接类为 None)。"""

    def __init__(self, message: str, code: Optional[str] = None):
        super().__init__(message)
        self.code = code


class LfasrNotConfigured(LfasrError):
    """XFYUN_APPID / XFYUN_LFASR_SECRET_KEY 未配置。"""


class LfasrCancelled(LfasrError):
    """用户取消等待(讯飞侧订单不受影响,会继续转完但结果没人取)。"""


def is_configured() -> bool:
    """LFASR 凭据是否齐 —— agent_builder 据此决定挂不挂 transcribe_audio tool。"""
    return all(
        (os.getenv(k) or "").strip()
        for k in ("XFYUN_APPID", "XFYUN_LFASR_SECRET_KEY")
    )


def _load_credentials() -> tuple[str, str]:
    appid = (os.getenv("XFYUN_APPID") or "").strip()
    secret = (os.getenv("XFYUN_LFASR_SECRET_KEY") or "").strip()
    if not (appid and secret):
        raise LfasrNotConfigured(
            "录音转写未配置:需在 .env 设 XFYUN_APPID / XFYUN_LFASR_SECRET_KEY"
            "(后者是讯飞控制台「语音转写」服务的 SecretKey,与语音听写的 key 不同)"
        )
    return appid, secret


def _auth_params() -> dict:
    appid, secret = _load_credentials()
    ts = str(int(time.time()))
    md5hex = hashlib.md5((appid + ts).encode()).hexdigest()
    signa = base64.b64encode(
        hmac.new(secret.encode(), md5hex.encode(), hashlib.sha1).digest()
    ).decode()
    return {"appId": appid, "ts": ts, "signa": signa}


def _post(path: str, params: dict, content: Optional[bytes] = None,
          timeout_s: float = 60) -> dict:
    """POST 一次并校验业务码;code != 000000 抛 LfasrError,返回 content 字段。"""
    try:
        resp = httpx.post(
            f"{_API_BASE}{path}",
            params={**_auth_params(), **params},
            content=content,
            headers={"Content-Type": "application/octet-stream"} if content else None,
            timeout=timeout_s,
        )
        resp.raise_for_status()
        body = resp.json()
    except LfasrError:
        raise
    except Exception as e:  # 连接 / 超时 / 非 JSON / HTTP 状态码
        raise LfasrError(f"讯飞录音转写连接失败:{type(e).__name__}: {e}")
    code = str(body.get("code") or "")
    if code != "000000":
        desc = body.get("descInfo") or "未知错误"
        raise LfasrError(f"讯飞录音转写失败({code}):{desc}", code=code)
    return body.get("content") or {}


def upload(data: bytes, file_name: str, *, language: str = "cn",
           role_type: int = 1) -> tuple[str, float]:
    """上传音频建转写订单。返回 (orderId, 预估转写耗时秒)。

    duration 参数按官方 demo 传定值(文档标必填但服务端不校验,只原样回显进
    orderInfo.originalDuration —— 真实时长看 realDuration);roleType=1 开说话人
    分离(不额外计费,会议场景直接可用)。
    """
    params = {
        "fileName": file_name,
        "fileSize": str(len(data)),
        "duration": "200",
        "language": language,
        "roleType": str(role_type),
    }
    content = _post("/upload", params, content=data, timeout_s=300)
    order_id = (content.get("orderId") or "").strip()
    if not order_id:
        raise LfasrError("讯飞录音转写:upload 成功但未返回 orderId")
    estimate_s = float(content.get("taskEstimateTime") or 0) / 1000
    return order_id, estimate_s


def get_result(order_id: str) -> dict:
    """查一次订单。返回 content(含 orderInfo.status / orderResult)。"""
    return _post("/getResult", {"orderId": order_id, "resultType": "transfer"},
                 timeout_s=60)


def parse_transcript(order_result: str) -> list[tuple[str, str]]:
    """orderResult(JSON 串)→ [(role, text)] 按时间顺序。

    lattice 每项的 json_1best 是**再编码一层的 JSON 串**(lattice2 里则已是对象),
    两种形态都兜。role 是说话人编号字符串("1"/"2"...,未分离时 "0" 或缺失 → "")。
    """
    try:
        parsed = json.loads(order_result or "{}")
    except json.JSONDecodeError as e:
        raise LfasrError(f"讯飞录音转写:结果 JSON 解析失败:{e}")
    lattice = parsed.get("lattice") or parsed.get("lattice2") or []
    segments: list[tuple[str, str]] = []
    for item in lattice:
        best = item.get("json_1best")
        if isinstance(best, str):
            try:
                best = json.loads(best)
            except json.JSONDecodeError:
                continue
        st = (best or {}).get("st") or {}
        text = "".join(
            cw.get("w", "")
            for rt in (st.get("rt") or [])
            for ws in (rt.get("ws") or [])
            for cw in (ws.get("cw") or [])
        )
        if not text.strip():
            continue
        role = str(st.get("rl") or "").strip()
        segments.append(("" if role == "0" else role, text))
    return segments


def format_transcript(segments: list[tuple[str, str]]) -> str:
    """[(role, text)] → 成稿文本。

    识别出 ≥2 个说话人时按连续同角色合并成「【说话人N】…」段落(会议/访谈场景);
    单说话人 / 未分离则纯文本拼接。
    """
    roles = {r for r, _ in segments if r}
    if len(roles) < 2:
        return "".join(t for _, t in segments).strip()
    lines: list[str] = []
    cur_role, buf = None, []
    for role, text in segments:
        if role != cur_role:
            if buf:
                lines.append(f"【说话人{cur_role or '?'}】{''.join(buf)}")
            cur_role, buf = role, [text]
        else:
            buf.append(text)
    if buf:
        lines.append(f"【说话人{cur_role or '?'}】{''.join(buf)}")
    return "\n\n".join(lines).strip()


def transcribe_file(
    data: bytes,
    file_name: str,
    *,
    language: str = "cn",
    cancel_check: Optional[Callable[[], bool]] = None,
) -> dict:
    """上传 + 轮询到订单完成。返回 {"text", "duration_s", "order_id", "elapsed_s"}。

    等待上限取 max(600s, 3×讯飞预估);轮询期连续网络错误 ≤ _POLL_ERR_TOLERANCE 次
    只记不抛(订单在讯飞侧,不能因一次抖动丢单)。取消抛 LfasrCancelled。
    """
    started = time.monotonic()
    order_id, estimate_s = upload(data, file_name, language=language)
    deadline = started + max(600.0, estimate_s * 3)
    consecutive_errs = 0
    while True:
        if cancel_check is not None and cancel_check():
            raise LfasrCancelled(f"用户取消(讯飞订单 {order_id} 会继续转完,但结果未取)")
        if time.monotonic() > deadline:
            raise LfasrError(
                f"讯飞录音转写超时(订单 {order_id} 等了 "
                f"{int(time.monotonic() - started)}s 仍未完成)"
            )
        time.sleep(_POLL_INTERVAL_S)
        try:
            content = get_result(order_id)
            consecutive_errs = 0
        except LfasrError as e:
            if e.code is not None:  # 业务错误码:重试无意义,直接抛
                raise
            consecutive_errs += 1
            if consecutive_errs > _POLL_ERR_TOLERANCE:
                raise
            continue
        info = content.get("orderInfo") or {}
        status = info.get("status")
        if status == -1:
            ft = info.get("failType")
            hint = _FAIL_HINTS.get(ft, f"转写失败(failType={ft})")
            raise LfasrError(f"讯飞录音转写失败:{hint}")
        if status != 4:
            continue  # 0 已创建 / 3 处理中
        segments = parse_transcript(content.get("orderResult") or "")
        # originalDuration 只是 upload 时 duration 参数的回显(我们传的定值);
        # realDuration 才是讯飞实际解出的音频时长(ms,即计费时长)—— 实测验证。
        return {
            "text": format_transcript(segments),
            "duration_s": float(info.get("realDuration") or 0) / 1000,
            "order_id": order_id,
            "elapsed_s": time.monotonic() - started,
        }
