"""
MIND OS 3.0 — 实时录音 WS 管线 (Phase B)

从 V2 asr-proxy.cjs 移植到 Python asyncio + aiohttp。

端点：GET /mindos-next/ws/record?token={JWT}&chatId={chatId}

协议（与 V2 客户端兼容）：
  客户端 → 服务端:  binary PCM16 帧 / {"type":"stop"}
  服务端 → 客户端:  {"type":"proxy_connected","chatId":"..."}
                   {"type":"partial","text":"..."}
                   {"type":"final","text":"..."}
                   {"type":"speech_started"/"speech_stopped"}
                   {"type":"asr_reconnecting","attempt":N}
                   {"type":"error","message":"..."}

DashScope Realtime API（V5 协议，与 V2 asr-proxy.cjs 完全相同）：
  wss://dashscope.aliyuncs.com/api-ws/v1/realtime?model=qwen3-asr-flash-realtime
  Auth: Authorization: bearer {DASHSCOPE_API_KEY}
  发送: {"type":"input_audio_buffer.append","audio":"<base64>"}
  停止: {"type":"input_audio_buffer.commit"} + {"type":"session.finish"}
  接收: session.created / conversation.item.input_audio_transcription.* /
        input_audio_buffer.speech_* / session.finished / error
"""

import asyncio
import base64
import json
import logging
import os
import time
from datetime import datetime
from pathlib import Path

from aiohttp import web

logger = logging.getLogger("dashscope_realtime")

DASHSCOPE_ASR_URL   = "wss://dashscope.aliyuncs.com/api-ws/v1/realtime"
DASHSCOPE_ASR_MODEL = os.getenv("DASHSCOPE_REALTIME_MODEL", "qwen3-asr-flash-realtime")
MAX_RECONNECTS      = 3
RECONNECT_BASE_MS   = 1000

# ★ 架构修复：这里直接存放 MindOSSSEServer 实例的引用。
# mindos_sse.py 在 start() 里导入本模块时调用 register_server(self)，
# 始终使用同一个对象，彻底绕开 sys.modules 命名空间问题。
_sse_server = None

def register_server(server) -> None:
    global _sse_server
    _sse_server = server
    logger.info("[Realtime] SSE server 已注入: %s", type(server).__name__)


# ─── 公开入口（由 mindos_sse.py 注册为路由） ─────────────────

async def handleWsRecord(request: web.Request) -> web.WebSocketResponse:
    """
    GET /mindos-next/ws/record?token={JWT}&chatId={chatId}
    aiohttp WS handler。
    """
    from gateway.platforms.mindos_sse import _verifyTokenAsync, _CORS_HEADERS  # type: ignore

    # 1. Auth via ?token= query param
    token = request.rel_url.query.get("token", "")
    chatId = request.rel_url.query.get("chatId", "")
    if not token:
        raise web.HTTPForbidden()

    user = await _verifyTokenAsync(token)
    if not user:
        raise web.HTTPUnauthorized()
    userId = user.get("sub") or user.get("userId", "")

    if not chatId:
        chatId = f"chat_{int(time.time() * 1000)}"

    meetingId = request.rel_url.query.get("meetingId") or f"rec_{int(time.time() * 1000)}"
    source   = request.rel_url.query.get("source", "mic")  # "mic" | "system"

    logger.info("[Realtime] 客户端连接 userId=%s chatId=%s meeting=%s source=%s", userId, chatId, meetingId, source)

    # 2. 升级到 WS
    clientWs = web.WebSocketResponse(heartbeat=30)
    await clientWs.prepare(request)

    # 3. 初始化 MD 文件（wiki/{userId}/raw/）
    wiki_root = os.getenv("MINDOS_WIKI_DIR", os.path.expanduser("~/.hermes/wiki"))
    wiki_dir  = Path(wiki_root) / userId / "raw"
    wiki_dir.mkdir(parents=True, exist_ok=True)

    now = datetime.now()
    suffix = "system" if source == "system" else "mic"
    md_filename = f"{now.strftime('%Y-%m-%d_%H%M')}_{suffix}_recording.md"
    md_file     = wiki_dir / md_filename
    rel_path    = f"raw/{md_filename}"

    # 写文件头
    source_label = "系统拾音" if source == "system" else "麦克风"
    md_file.write_text(
        f"# 录音转写 {now.strftime('%Y-%m-%d %H:%M')}（{source_label}）\n\n> 实时录制于 MindOS\n\n",
        encoding="utf-8",
    )

    total_chars  = 0
    sentences    = []          # 收集完整句子
    reconnects   = 0
    ds_ready     = asyncio.Event()
    stop_event   = asyncio.Event()
    audio_queue: asyncio.Queue[bytes | None] = asyncio.Queue()

    # ─── DashScope WS 协程 ──────────────────────────────────

    async def run_dashscope():
        nonlocal reconnects, total_chars

        api_key = os.getenv("DASHSCOPE_API_KEY", "")
        if not api_key:
            await _send_client(clientWs, {"type": "error", "message": "ASR 未配置 DASHSCOPE_API_KEY"})
            return

        import aiohttp as _aio
        url = f"{DASHSCOPE_ASR_URL}?model={DASHSCOPE_ASR_MODEL}"
        headers = {"Authorization": f"bearer {api_key}"}

        while reconnects <= MAX_RECONNECTS and not stop_event.is_set():
            try:
                async with _aio.ClientSession() as session:
                    async with session.ws_connect(url, headers=headers) as dsWs:
                        ds_ready.clear()   # 连接成功，但等 session.created 后再就绪
                        reconnects = 0  # 连上就重置
                        logger.info("[Realtime] DashScope 已连接 meeting=%s", meetingId)

                        async def send_audio_loop():
                            """从队列取音频帧发给 DashScope"""
                            while True:
                                chunk = await audio_queue.get()
                                if chunk is None:
                                    # stop 信号
                                    try:
                                        await dsWs.send_str(json.dumps(
                                            {"type": "input_audio_buffer.commit"}))
                                        await dsWs.send_str(json.dumps(
                                            {"type": "session.finish"}))
                                    except Exception:
                                        pass
                                    return
                                if dsWs.closed:
                                    return
                                try:
                                    b64 = base64.b64encode(chunk).decode()
                                    await dsWs.send_str(json.dumps(
                                        {"type": "input_audio_buffer.append", "audio": b64}))
                                except Exception as e:
                                    logger.warning("[Realtime] send audio err: %s", e)

                        # 并发：发音频 + 收结果
                        send_task = asyncio.create_task(send_audio_loop())

                        async for msg in dsWs:
                            if msg.type != _aio.WSMsgType.TEXT:
                                continue
                            try:
                                evt = json.loads(msg.data)
                            except Exception:
                                continue

                            etype = evt.get("type", "")

                            if etype == "session.created":
                                # V2 验证：DashScope V5 Realtime 不需要 session.update
                                # 直接就绪，开始接收音频帧
                                ds_ready.set()
                                await _send_client(clientWs, {
                                    "type": "proxy_connected", "chatId": chatId})

                            elif etype == "conversation.item.input_audio_transcription.completed":
                                text = (evt.get("transcript") or "").strip()
                                if text:
                                    sentences.append(text)
                                    total_chars += len(text)
                                    elapsed = int(time.time() * 1000 - start_ms) // 1000
                                    mm, ss = divmod(elapsed, 60)
                                    # 实时写 MD
                                    with md_file.open("a", encoding="utf-8") as f:
                                        f.write(f"[{mm:02d}:{ss:02d}] {text}\n")
                                    await _send_client(clientWs, {"type": "final", "text": text})

                            elif etype == "conversation.item.input_audio_transcription.text":
                                text = (evt.get("transcript") or "").strip()
                                if text:
                                    await _send_client(clientWs, {"type": "partial", "text": text})

                            elif etype in (
                                "input_audio_buffer.speech_started",
                                "input_audio_buffer.speech_stopped",
                            ):
                                short = "speech_started" if "started" in etype else "speech_stopped"
                                await _send_client(clientWs, {"type": short})

                            elif etype == "session.finished":
                                logger.info("[Realtime] session.finished meeting=%s", meetingId)
                                send_task.cancel()
                                stop_event.set()
                                return

                            elif etype == "error":
                                err_msg = evt.get("error", {}).get("message") or str(evt)
                                logger.error("[Realtime] DashScope error: %s", err_msg)
                                await _send_client(clientWs, {"type": "error", "message": err_msg})

                        send_task.cancel()

            except Exception as e:
                logger.warning("[Realtime] DashScope 连接异常: %s", e)

            if stop_event.is_set():
                return

            # 重连
            reconnects += 1
            if reconnects > MAX_RECONNECTS:
                logger.error("[Realtime] 重连次数耗尽 meeting=%s", meetingId)
                break

            delay = RECONNECT_BASE_MS * (2 ** (reconnects - 1)) / 1000
            logger.warning("[Realtime] %ss 后重连(%d/%d) meeting=%s",
                           delay, reconnects, MAX_RECONNECTS, meetingId)
            await _send_client(clientWs, {
                "type": "asr_reconnecting",
                "attempt": reconnects,
                "maxAttempts": MAX_RECONNECTS,
            })
            ds_ready.clear()
            await asyncio.sleep(delay)

    # ─── 主循环：接收客户端消息 ──────────────────────────────

    start_ms = int(time.time() * 1000)
    ds_task  = asyncio.create_task(run_dashscope())

    import aiohttp as _aio2
    async for msg in clientWs:
        if msg.type == _aio2.WSMsgType.BINARY:
            # PCM16 帧 → 入队
            await ds_ready.wait()   # 等 DashScope 握手完成再转发
            await audio_queue.put(msg.data)

        elif msg.type == _aio2.WSMsgType.TEXT:
            try:
                cmd = json.loads(msg.data)
                if cmd.get("type") == "stop":
                    logger.info("[Realtime] 收到 stop 命令 meeting=%s", meetingId)
                    await audio_queue.put(None)   # 通知 send_audio_loop 发 commit+finish
            except Exception:
                pass

        elif msg.type in (_aio2.WSMsgType.ERROR, _aio2.WSMsgType.CLOSE):
            break

    # 客户端断开：确保停止
    stop_event.set()
    await audio_queue.put(None)
    try:
        await asyncio.wait_for(ds_task, timeout=10)
    except (asyncio.TimeoutError, Exception):
        pass

    # ─── Finalize：写 MD 结尾 + 写 DB + 推 SSE ──────────────

    # P0-4: 计算实时录音总时长
    duration_seconds = (time.time() * 1000 - start_ms) / 1000.0

    await _finalize(
        md_file=md_file,
        rel_path=rel_path,
        sentences=sentences,
        total_chars=total_chars,
        chatId=chatId,
        meetingId=meetingId,
        userId=userId,
        duration_seconds=duration_seconds,
        source=source,
    )

    logger.info("[Realtime] 会话结束 meeting=%s chars=%d", meetingId, total_chars)
    return clientWs


# ─── 内部工具函数 ─────────────────────────────────────────────

async def _send_client(ws: web.WebSocketResponse, data: dict) -> None:
    """安全发送 JSON 到客户端"""
    if ws.closed:
        return
    try:
        await ws.send_str(json.dumps(data, ensure_ascii=False))
    except Exception:
        pass


async def _finalize(
    md_file: Path,
    rel_path: str,
    sentences: list[str],
    total_chars: int,
    chatId: str,
    meetingId: str,
    userId: str,
    duration_seconds: float = 0.0,
    source: str = "mic",
) -> None:
    """写 MD 结尾、持久化到 DB、推送 SSE md:appended。

    使用 voice2md_atoms 共享原子层（④ db_persist + ⑤ sse_push）。
    """
    from voice2md_atoms import (  # type: ignore
        persist_audio_result, push_md_appended, deduct_asr_credits,
    )

    # 写结尾标记
    try:
        with md_file.open("a", encoding="utf-8") as f:
            f.write("\n---（录音结束）\n")
    except Exception as e:
        logger.warning("[Realtime] 写 MD 结尾失败: %s", e)

    # 读完整内容（用于内嵌推送）
    md_content = ""
    try:
        md_content = md_file.read_text(encoding="utf-8")
    except Exception:
        pass

    # ④ db_persist（chars=0 也写 DB，确保刷新后可恢复）
    persist_audio_result(
        chat_id=chatId, user_id=userId,
        file_name=md_file.name, md_path=rel_path,
        chars=total_chars, md_content=md_content,
        oss_read_url="",  # 实时录音无 OSS URL
    )

    # 积分扣减（实时录音：2 credits/秒）
    asr_credits = max(1, int(duration_seconds * 2))
    deduct_asr_credits(
        user_id=userId, chat_id=chatId, credits=asr_credits,
        tx_type="asr_realtime", model="qwen3-asr-flash-realtime",
        seconds=duration_seconds,
    )

    # ⑤ sse_push
    # ★ 使用模块级 _sse_server（由 mindos_sse.start() 直接注入）
    push_md_appended(
        sse_server=_sse_server, user_id=userId, chat_id=chatId,
        file=rel_path, chars=total_chars, md_content=md_content,
        message=f"✅ 录音转写完成（{total_chars} 字）",
        source=source,
    )

