"""微信双渠道路由:ClawBot 个人微信 + 企业微信(§8.7)。

绑定/解绑/测试推送 + 企微 OAuth(绑定与免登两路)+ 企微入站回调。
入站对话核心在 web/runs.py::run_channel_conversation(渠道无关,两渠道共用)。
"""
from __future__ import annotations

import asyncio
import os
from uuid import UUID

from fastapi import Depends, HTTPException, Request

from ..auth import AuthConfig, mint_token
from ..common import BRAND, iso
from ..runs import run_channel_conversation, transcribe_wecom_voice

# 免登入口 state 里的哨兵值:登录流程没有 user_id,用它占位 + 防 CSRF。
# 真实 user_id 都是 UUID 串,不会与之撞;绑定回调那侧也显式拒绝这个值。
_WECOM_LOGIN_STATE = "login"


def _wecom_page(msg: str, ok: bool):
    """OAuth 回跳落地提示页(无 JWT 场景:绑定 / 免登失败都用)。"""
    from fastapi.responses import HTMLResponse
    color = "#1a7f37" if ok else "#cf222e"
    return HTMLResponse(
        f"<!doctype html><meta charset=utf-8><meta name=viewport content='width=device-width,initial-scale=1'>"
        f"<div style='font:16px system-ui;max-width:420px;margin:80px auto;text-align:center;color:{color}'>"
        f"{msg}</div><div style='text-align:center;color:#888;font:13px system-ui'>可关闭本页返回{BRAND}</div>"
    )


def register_wechat_routes(app, *, require_user, auth_cfg: AuthConfig) -> None:
    # ───────────── 微信接入(ClawBot,§8.7)─────────────

    @app.post("/v1/wechat/bind/qrcode", tags=["wechat"])
    async def wechat_bind_qrcode(user_id: UUID = Depends(require_user)):
        """起一张 ClawBot 绑定二维码(渲成 PNG data-uri)。前端展示,用户手机微信扫;
        二维码 TTL ~1min,前端轮询到 expired 后重调本端点换码。"""
        import base64 as _b64
        import io as _io

        import segno

        from core.wechat import ilink
        qr = await asyncio.to_thread(ilink.get_bot_qrcode)
        buf = _io.BytesIO()
        segno.make(qr.deeplink, error="m").save(buf, kind="png", scale=6, border=3)
        data_uri = "data:image/png;base64," + _b64.b64encode(buf.getvalue()).decode()
        return {"qrcode_id": qr.qrcode_id, "qr_png": data_uri}

    @app.get("/v1/wechat/bind/status", tags=["wechat"])
    async def wechat_bind_status(qrcode_id: str, user_id: UUID = Depends(require_user)):
        """轮询扫码状态(服务端长轮询,hold 数十秒)。confirmed → 写绑定。
        返回 {status: wait|confirmed|expired};expired 时前端重起二维码。"""
        from core.wechat import ilink
        from core.wechat import service as _wx
        res = await asyncio.to_thread(ilink.poll_qrcode_status, qrcode_id)
        if res.status == "confirmed" and res.bot_token:
            await asyncio.to_thread(
                _wx.upsert_clawbot_binding, user_id, res.bot_token,
                res.base_url or ilink.DEFAULT_BASE,
            )
        return {"status": res.status}

    @app.get("/v1/wechat/bind", tags=["wechat"])
    def wechat_bind_get(user_id: UUID = Depends(require_user)):
        """当前用户的微信绑定状态(不泄露 token)。"""
        from core.wechat import service as _wx
        snap = _wx.get_binding(user_id)
        if snap is None or snap.status != "active":
            return {"bound": False}
        return {
            "bound": True,
            "user_im_id": snap.user_im_id,
            "can_push": bool(_wx._token_fresh(snap)),   # 24h 窗口内可主动推
            "last_active": iso(snap.context_token_at),
        }

    @app.delete("/v1/wechat/bind", status_code=204, tags=["wechat"])
    def wechat_unbind(user_id: UUID = Depends(require_user)):
        from core.wechat import service as _wx
        _wx.unbind(user_id)
        return

    @app.post("/v1/wechat/test", tags=["wechat"])
    async def wechat_test(user_id: UUID = Depends(require_user)):
        """自检:给已绑用户推一条测试消息(需用户近 24h 在微信开口过)。"""
        from core.wechat import service as _wx
        res = await asyncio.to_thread(
            _wx.push_clawbot, user_id, f"{BRAND} 测试消息:微信绑定成功,推送通道正常。"
        )
        return {"ok": res.ok, "reason": res.reason}

    # ───────────── 企业微信接入(渠道 B,纯推送,§8.7)─────────────

    @app.get("/v1/wecom/oauth/url", tags=["wecom"])
    def wecom_oauth_url(request: Request, user_id: UUID = Depends(require_user)):
        """生成企业微信 OAuth 网页授权链接(前端打开 → 扫码授权)。回调用 state 带回身份。
        redirect 主机须在应用「网页授权可信域名」内;默认取 ZCBOT_PUBLIC_BASE_URL 或请求 base。"""
        from core.wechat import wecom
        if not wecom.wecom_configured():
            raise HTTPException(400, "企业微信未配置(需 WECOM_CORPID/AGENTID/SECRET)")
        base = (os.getenv("ZCBOT_PUBLIC_BASE_URL", "").strip()
                or str(request.base_url).rstrip("/"))
        redirect_uri = f"{base}/v1/wecom/oauth/callback"
        state = wecom.sign_state(str(user_id))
        return {"authorize_url": wecom.oauth_authorize_url(redirect_uri, state)}

    @app.get("/v1/wecom/oauth/callback", include_in_schema=False)
    async def wecom_oauth_callback(code: str = "", state: str = ""):
        """企业微信授权后浏览器回跳到这(无 JWT;身份从 state 验)。换 userid → 写绑定 → 回提示页。"""
        from core.wechat import service as _wx
        from core.wechat import wecom

        uid = wecom.verify_state(state)
        if not uid or uid == _WECOM_LOGIN_STATE:
            return _wecom_page(f"绑定失败:授权已过期或无效,请回{BRAND}重试。", False)
        if not code:
            return _wecom_page("绑定失败:未拿到授权码。", False)
        try:
            wecom_userid = await asyncio.to_thread(wecom.get_user_id, code)
        except Exception as e:
            return _wecom_page(f"绑定失败:{type(e).__name__}", False)
        if not wecom_userid:
            return _wecom_page("绑定失败:你不是该企业成员(只支持企业内成员)。", False)
        await asyncio.to_thread(_wx.upsert_wecom_binding, UUID(uid), wecom_userid)
        return _wecom_page("✅ 企业微信绑定成功!以后简报 / 结果会推到你的企业微信。", True)

    @app.get("/v1/wecom/entry", include_in_schema=False)
    def wecom_entry(request: Request):
        """企业微信应用主页免登入口(无 JWT):302 到 OAuth 授权,回调换 JWT 直进 embed 控制台。

        企微后台「应用主页」填 `<公网 base>/v1/wecom/entry`。企微客户端内(UA 带 wxwork)
        走网页授权 snsapi_base 静默无感;外部浏览器退到 wwlogin 扫码,同一 URL 两端可用。
        网页授权要求域名登记在应用「网页授权及 JS-SDK 可信域名」(与扫码的授权登录域名是两项)。"""
        from fastapi.responses import RedirectResponse
        from core.wechat import wecom
        if not wecom.wecom_configured():
            raise HTTPException(404, "企业微信未配置(需 WECOM_CORPID/AGENTID/SECRET)")
        base = (os.getenv("ZCBOT_PUBLIC_BASE_URL", "").strip()
                or str(request.base_url).rstrip("/"))
        redirect_uri = f"{base}/v1/wecom/entry/callback"
        state = wecom.sign_state(_WECOM_LOGIN_STATE)
        ua = (request.headers.get("user-agent") or "").lower()
        url = (wecom.oauth_inclient_url(redirect_uri, state) if "wxwork" in ua
               else wecom.oauth_authorize_url(redirect_uri, state))
        return RedirectResponse(url, status_code=302)

    @app.get("/v1/wecom/entry/callback", include_in_schema=False)
    async def wecom_entry_callback(code: str = "", state: str = ""):
        """免登回跳:code → wecom_userid → 反查绑定 → 签 JWT → 302 进 embed 控制台。

        token 放 URL fragment(不进服务端日志、不随后续请求发出,前端读完立即清 hash)。
        未绑定成员不自动建号 —— 提示先在控制台完成绑定,避免同一人产生重复账号。"""
        from fastapi.responses import RedirectResponse
        from core.wechat import service as _wx
        from core.wechat import wecom

        if wecom.verify_state(state) != _WECOM_LOGIN_STATE:
            return _wecom_page("登录失败:授权已过期或无效,请重新打开应用。", False)
        if not code:
            return _wecom_page("登录失败:未拿到授权码。", False)
        try:
            wecom_userid = await asyncio.to_thread(wecom.get_user_id, code)
        except Exception as e:
            return _wecom_page(f"登录失败:{type(e).__name__}", False)
        if not wecom_userid:
            return _wecom_page("登录失败:你不是该企业成员(只支持企业内成员)。", False)
        uid = await asyncio.to_thread(_wx.get_user_by_wecom_userid, wecom_userid)
        if uid is None:
            return _wecom_page(
                f"该企业微信还未绑定{BRAND}账号:请先在电脑端{BRAND}控制台登录,"
                "点左栏「企业微信」渠道卡片完成绑定,再回来打开本应用。", False)
        token, _exp = mint_token(auth_cfg, uid)
        # JWT 是 base64url + '.',UUID 是 hex + '-',都无需再转义,fragment 安全
        return RedirectResponse(
            f"/static/dev.html?embed=1&wecom=1#token={token}&user_id={uid}",
            status_code=302,
        )

    @app.get("/v1/wecom/bind", tags=["wecom"])
    def wecom_bind_get(user_id: UUID = Depends(require_user)):
        """当前用户企业微信绑定状态。"""
        from core.wechat import service as _wx
        from core.wechat import wecom
        wuid = _wx.get_wecom_userid(user_id)
        return {"configured": wecom.wecom_configured(), "bound": bool(wuid), "wecom_userid": wuid}

    @app.put("/v1/wecom/bind/userid", tags=["wecom"])
    def wecom_bind_userid(payload: dict, user_id: UUID = Depends(require_user)):
        """手填企业微信成员 userid 绑定(无 HTTPS 域名 / 不走 OAuth 时用)。
        userid 见管理后台 → 通讯录 → 点成员 → 「账号」。"""
        from core.wechat import service as _wx
        from core.wechat import wecom
        if not wecom.wecom_configured():
            raise HTTPException(400, "企业微信未配置(需 WECOM_CORPID/AGENTID/SECRET)")
        wuid = (payload.get("wecom_userid") or "").strip()
        if not wuid:
            raise HTTPException(400, "wecom_userid 不能为空")
        _wx.upsert_wecom_binding(user_id, wuid)
        return {"bound": True, "wecom_userid": wuid}

    @app.delete("/v1/wecom/bind", status_code=204, tags=["wecom"])
    def wecom_unbind(user_id: UUID = Depends(require_user)):
        from core.wechat import service as _wx
        _wx.unbind_wecom(user_id)
        return

    @app.post("/v1/wecom/test", tags=["wecom"])
    async def wecom_test(user_id: UUID = Depends(require_user)):
        """自检:给已绑用户推一条企业微信测试消息(无 24h 窗口约束)。"""
        from core.wechat import service as _wx
        res = await asyncio.to_thread(
            _wx.push_wecom, user_id, f"{BRAND} 测试消息:企业微信绑定成功,推送通道正常。"
        )
        return {"ok": res.ok, "reason": res.reason}

    # ── 企业微信「接收消息」回调(入站对话,§8.7)── 无 JWT;身份从加密 XML 的 FromUserName 反查。
    # 配置:企业微信后台「应用 → 接收消息 → 设置 API 接收」填本 URL + Token + EncodingAESKey,
    # 对应 env WECOM_CALLBACK_TOKEN / WECOM_CALLBACK_AESKEY。回调 URL = <公网 base>/v1/wecom/callback。
    @app.get("/v1/wecom/callback", include_in_schema=False)
    def wecom_callback_verify(
        msg_signature: str = "", timestamp: str = "", nonce: str = "", echostr: str = ""
    ):
        """企业微信保存回调配置时 GET 验有效性:验签 + 解密 echostr,原样回明文。"""
        from fastapi.responses import PlainTextResponse
        from core.wechat import wecom, wecom_crypto
        if not wecom_crypto.callback_configured():
            raise HTTPException(404, "wecom callback 未配置(需 WECOM_CALLBACK_TOKEN/AESKEY)")
        try:
            plain = wecom_crypto.verify_url(
                msg_signature, timestamp, nonce, echostr, corpid=wecom._corpid()
            )
        except Exception as e:  # noqa: BLE001
            raise HTTPException(400, f"verify failed: {type(e).__name__}: {e}")
        return PlainTextResponse(plain)

    @app.post("/v1/wecom/callback", include_in_schema=False)
    async def wecom_callback(
        request: Request, msg_signature: str = "", timestamp: str = "", nonce: str = ""
    ):
        """企业微信推入站消息(加密 XML POST)。解密 → 反查身份 → 后台跑 agent → 主动推回。

        agent 跑 >5s,远超被动回复(同步返回密文 XML)5s 窗口 → 异步:立刻回 'success' 防重试,
        agent 结果走 wecom.send_text 主动推回(message/send,无 24h 窗口约束)。同一用户的并发/
        重复投递由对话 task 的 run 锁挡(第二条会收到「上一条还在处理中」)。
        """
        from fastapi.responses import PlainTextResponse
        from core.wechat import service as _wx
        from core.wechat import wecom, wecom_crypto
        from core.wechat.ilink import InboundAttachment
        if not wecom_crypto.callback_configured():
            raise HTTPException(404, "wecom callback 未配置(需 WECOM_CALLBACK_TOKEN/AESKEY)")
        body = (await request.body()).decode("utf-8")
        try:
            msg = wecom_crypto.decrypt_message(
                msg_signature, timestamp, nonce, body, corpid=wecom._corpid()
            )
        except Exception as e:  # noqa: BLE001
            raise HTTPException(400, f"decrypt failed: {type(e).__name__}: {e}")
        msgtype = msg.get("MsgType") or ""
        wuid = msg.get("FromUserName") or ""
        # 每条解密成功的入站都留痕(text 路径原先零日志)。排查"发某类消息没反应"先看
        # 这行:没出现 = 企微侧根本没投递该回调,与后面的处理代码无关。
        print(f"[wecom] inbound msgtype={msgtype!r} from={wuid} "
              f"event={msg.get('Event') or '-'}")
        uid = await asyncio.to_thread(_wx.get_user_by_wecom_userid, wuid)
        if uid is None:
            # 未绑定成员发真实消息(菜单/事件不算)→ 回绑定指引,不再静默:聊天优先布局
            # (不配应用主页)下新员工点应用直进会话就打字,静默会被当成 bot 坏了。
            # 这是对入站消息的应答,每条都回(不走 wecom_welcomes 去重),永远有反馈。
            if wuid and msgtype in ("text", "image", "file", "voice"):
                try:
                    await asyncio.to_thread(
                        wecom.send_text, wuid,
                        f"你还未绑定{BRAND}账号:请先在电脑端浏览器登录{BRAND}控制台,"
                        "点左栏「企业微信」渠道卡片完成绑定;绑定后在这里发消息即可对话。")
                except Exception as e:  # noqa: BLE001 —— 指引推送失败只打日志
                    print(f"[wecom] unbound guidance push failed: {type(e).__name__}: {e}")
            return PlainTextResponse("success")

        # 文本取 Content;图片/文件走 media/get 下载,构造 InboundAttachment(与个人微信同结构,
        # 仅 kind/file_name/data 三字段被 run_channel_conversation 用到)。语音只登记 MediaId,
        # 下载/解码/转写全放后台任务 —— 长语音转写要数秒,inline 会踩企业微信 5s 回调窗口触发
        # 重推。其余类型(视频/位置/链接/事件)暂不处理,回 success 防重试。
        content = ""
        attachments: list = []
        voice_media_id = ""
        if msgtype == "text":
            content = (msg.get("Content") or "").strip()
        elif msgtype in ("image", "file"):
            media_id = msg.get("MediaId") or ""
            if media_id:
                try:
                    data, fname = await asyncio.to_thread(wecom.download_media, media_id)
                    attachments.append(InboundAttachment(
                        kind=("image" if msgtype == "image" else "file"),
                        media={},
                        file_name=(msg.get("FileName") or fname or ""),
                        data=data,
                    ))
                except Exception as e:  # noqa: BLE001
                    print(f"[wecom] {wuid} download {msgtype} err: {type(e).__name__}: {e}")
        elif msgtype == "voice":
            voice_media_id = msg.get("MediaId") or ""
            print(f"[wecom-voice] {wuid} inbound MediaId="
                  f"{'set' if voice_media_id else 'MISSING'} Format={msg.get('Format') or '?'}")
        else:
            # 静默跳过但留一行日志:排查"发了某类型消息没反应"全靠它
            print(f"[wecom] {wuid} unhandled msgtype={msgtype!r}")
            return PlainTextResponse("success")
        if not content and not attachments and not voice_media_id:
            return PlainTextResponse("success")   # 空消息 / 附件下载全失败 → 静默

        async def _push_checked(uid, text):
            """主动推一条并把失败落日志(push_wecom 不抛错,失败只在返回值里)。"""
            res = await asyncio.to_thread(_wx.push_wecom, uid, text)
            if not res.ok:
                print(f"[wecom] push failed: {res.reason}")

        async def _bg(uid=uid, content=content, attachments=attachments,
                      voice_media_id=voice_media_id):
            if voice_media_id:
                # 语音:先转写成文本再走同一条对话链路。识别结果必须回显 —— 用户在微信端
                # 看不到自己被识别成了什么,识别错时不回显会一头雾水。
                try:
                    content = await transcribe_wecom_voice(voice_media_id)
                except Exception as e:  # noqa: BLE001
                    from core.asr_xfyun import XfyunASRError
                    from core.audio import AudioConvertError
                    print(f"[wecom-voice] transcribe err: {type(e).__name__}: {e}")
                    msg_txt = (str(e) if isinstance(e, (XfyunASRError, AudioConvertError))
                               else f"{type(e).__name__}: {e}")
                    await _push_checked(uid, f"[语音识别失败] {msg_txt}")
                    return
                if not content:
                    await _push_checked(uid, "没听清语音内容,请再说一次,或改用文字。")
                    return
                await _push_checked(uid, f"🎤 已识别:{content}")
            try:
                reply = await run_channel_conversation(
                    app, uid, content, attachments, channel="wecom")
            except Exception as e:  # noqa: BLE001
                reply = f"[出错] {type(e).__name__}: {e}"
            if reply and reply.strip():
                # 最终回复也走 _push_checked:message/send 失败(如企微「企业可信 IP」
                # 拦截,errcode 60020)原先返回值被忽略纯静默,排查无从下手
                await _push_checked(uid, reply)

        # 登记到 inflight:持强引用防 task 被 GC 中途回收 + 关停时 drain(value=None → 不参与
        # broker cancel;内层 run_agent_bg runner 另有自己的 inflight 项负责取消)。
        bg = asyncio.create_task(_bg(), name=f"wecom-msg-{str(uid)[:8]}")
        app.state.inflight[bg] = None
        bg.add_done_callback(lambda t: app.state.inflight.pop(t, None))
        return PlainTextResponse("success")
