"""入站语音解码 + 转写:任意来源语音字节 → 16k/16bit/mono PCM → 讯飞 IAT 文本。

微信系渠道的语音格式:
- 企业微信 `media/get` 下发 AMR(AMR-NB 8kHz,`#!AMR\\n` 头)—— ffmpeg 原生解码器可解,
  无需 opencore 编译选项;
- 个人微信 ClawBot `voice_item` 下发 SILK v3(`\\x02#!SILK_V3` 头,微信/QQ 私有变体,
  ffmpeg 解不了)—— pilk 解码(24kHz PCM,社区实测微信采样率)再 ffmpeg 重采样到 16k。

对外三个入口:
- `to_pcm16k(data)`:ffmpeg 能认的容器(AMR/MP3/WAV/OGG...)→ 16k PCM;
- `decode_voice(data)`:按字节头分流 SILK/其它 → 16k PCM;
- `transcribe_voice(data)`(async):decode_voice + 讯飞整段转写,渠道层只调这一个。

ffmpeg 是**系统依赖**(Linux `apt install ffmpeg`;Windows 开发机 winget 装或设
FFMPEG_PATH 指向可执行文件),未安装时抛 AudioNotConfigured,上层给可读提示。
"""
from __future__ import annotations

import asyncio
import os
import shutil
import subprocess

_TIMEOUT_S = 30  # 60s 语音的解码远快于实时,30s 兜底防挂死
_SILK_MAGIC = b"#!SILK_V3"
_SILK_RATE = 24000  # 微信 silk 解码采样率(kn007/silk-v3-decoder 等社区工具同款)


class AudioConvertError(RuntimeError):
    """音频解码失败(格式不识别 / ffmpeg 报错)。"""


class AudioNotConfigured(AudioConvertError):
    """解码器不可用(缺 ffmpeg / 缺 pilk)—— 部署缺依赖,提示运维安装。"""


def find_ffmpeg() -> str:
    """定位 ffmpeg 可执行文件:env FFMPEG_PATH 优先,其次 PATH。找不到抛 AudioNotConfigured。"""
    cand = (os.getenv("FFMPEG_PATH") or "").strip() or shutil.which("ffmpeg")
    if not cand or not (os.path.isfile(cand) or shutil.which(cand)):
        raise AudioNotConfigured(
            "语音解码不可用:未找到 ffmpeg(Linux 服务器 `apt install ffmpeg`;"
            "或设 FFMPEG_PATH 指向 ffmpeg 可执行文件)"
        )
    return cand


def _run_ffmpeg(in_args: list[str], data: bytes) -> bytes:
    """跑一趟 ffmpeg:stdin 喂 data,stdout 收 16k/16bit/mono 小端 PCM。"""
    cmd = [
        find_ffmpeg(), "-hide_banner", "-loglevel", "error",
        *in_args, "-i", "pipe:0",
        "-f", "s16le", "-ar", "16000", "-ac", "1",
        "pipe:1",
    ]
    try:
        proc = subprocess.run(
            cmd, input=data, capture_output=True, timeout=_TIMEOUT_S
        )
    except subprocess.TimeoutExpired:
        raise AudioConvertError(f"音频解码超时(>{_TIMEOUT_S}s)")
    except OSError as e:
        raise AudioNotConfigured(f"ffmpeg 启动失败:{type(e).__name__}: {e}")
    if proc.returncode != 0 or not proc.stdout:
        err = (proc.stderr or b"").decode("utf-8", errors="replace").strip()
        raise AudioConvertError(f"音频解码失败:{err[:300] or '无输出'}")
    return proc.stdout


def to_pcm16k(data: bytes) -> bytes:
    """音频字节(AMR/MP3/WAV/OGG 等 ffmpeg 认识的格式)→ 16kHz/16bit/单声道小端 PCM。

    容器格式由 ffmpeg 从字节头自动探测,无需调用方声明。空输入返回 b""。
    """
    if not data:
        return b""
    return _run_ffmpeg([], data)


def is_silk(data: bytes) -> bool:
    """微信/QQ 的 SILK v3:裸 `#!SILK_V3` 或前置一个 0x02 字节的腾讯变体。"""
    return data[:9] == _SILK_MAGIC or data[1:10] == _SILK_MAGIC


def _silk_to_pcm16k(data: bytes) -> bytes:
    """SILK v3 → 16k PCM:pilk 解码(只认文件路径,过一趟临时文件)+ ffmpeg 重采样。

    pilk 0.2.4 实测:decode 直接吃带 0x02 前缀的腾讯变体,无需剥前缀。
    """
    try:
        import pilk
    except ImportError:
        raise AudioNotConfigured(
            "语音解码不可用:缺 pilk(`pip install -r requirements.txt` 或 pip install pilk)"
        )
    import tempfile
    with tempfile.TemporaryDirectory(prefix="zcbot-silk-") as td:
        silk_p = os.path.join(td, "in.silk")
        pcm_p = os.path.join(td, "out.pcm")
        with open(silk_p, "wb") as f:
            f.write(data)
        try:
            pilk.decode(silk_p, pcm_p, pcm_rate=_SILK_RATE)
        except Exception as e:  # noqa: BLE001 —— pilk.error 等
            raise AudioConvertError(f"SILK 解码失败:{type(e).__name__}: {e}")
        with open(pcm_p, "rb") as f:
            pcm24 = f.read()
    if not pcm24:
        return b""
    return _run_ffmpeg(["-f", "s16le", "-ar", str(_SILK_RATE), "-ac", "1"], pcm24)


def decode_voice(data: bytes) -> bytes:
    """入站语音字节 → 16k PCM:SILK(个人微信)走 pilk,其余(企微 AMR 等)走 ffmpeg 探测。"""
    if not data:
        return b""
    if is_silk(data):
        return _silk_to_pcm16k(data)
    return to_pcm16k(data)


async def transcribe_voice(data: bytes) -> str:
    """入站语音字节 → 识别文本(渠道层唯一入口)。

    解码放线程池(子进程/文件 IO);超讯飞 60s 上限截前 60s(微信系语音本身也限 60s)。
    失败抛 AudioConvertError / XfyunASRError,message 可直接给用户看。
    """
    from core import asr_xfyun

    pcm = await asyncio.to_thread(decode_voice, data)
    return await asr_xfyun.transcribe(pcm[: asr_xfyun.MAX_PCM_BYTES])
