#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
微信 174 action 能力矩阵 生成器

把「Hook catalog 探针报告（probe 假参数）」+ ACTION_TO_RPC 映射 + JS intent 分类
合成一张**诚实的能力矩阵**：每个 action → 模块 / 通道 / 可调用条件，
让存客宝 / 触客宝 / AI数智员工 / SuperAdmin 知道「这个动作怎么调、需要什么」。

诚实口径（真机铁律 #3/#14）：
  - probe 用占位参数，写操作 success:False 是**正常**（需真实参数/上下文），非代码缺陷；
  - 本矩阵据此区分「已验可直调 / 需真实参数 / 需UI或测试号 / companion模块」。

用法：
  python3 sdk/scripts/gen_wechat_capability_matrix.py \
      --report sdk/tmp/matrix_hook_catalog_xgfe65eimrrofyws_1780127048.json
输出：开发文档/5、接口/03-Hook与微信/微信174动作能力矩阵.md
"""
from __future__ import annotations
import argparse, datetime, glob, json, os, re

ROOT = os.path.normpath(os.path.join(os.path.dirname(__file__), "..", ".."))
HOOK_EXEC = os.path.join(ROOT, "sdk", "agent", "hook", "hook_executor.py")
HOOK_JS = os.path.join(ROOT, "sdk", "agent", "hook", "wechat_hook_v2.js")
OUT = os.path.join(ROOT, "开发文档", "5、接口", "03-Hook与微信", "微信174动作能力矩阵.md")

# 资金/隐私敏感（需测试号 + 小额 + 审计）
MONEY = {"send_red_packet", "receive_red_packet", "transfer", "send_transfer",
         "receive_transfer", "payment_receive", "receive_payment"}
# 需相机/系统 UI（纯 Frida 难覆盖，u2/companion）
UI_HEAVY = {"set_avatar", "scan_qr_code", "scan_add_friend", "scan_extract_qr",
            "extract_qr_from_image", "add_friend_by_qr", "show_payment_code",
            "voice_call", "video_call", "send_voice", "send_voice_message"}


def parse_action_to_rpc() -> dict:
    pairs, inb = {}, False
    for ln in open(HOOK_EXEC, encoding="utf-8"):
        if re.match(r"\s*ACTION_TO_RPC\s*[:=]", ln):
            inb = True; continue
        if inb:
            if re.match(r"^\}", ln):
                break
            m = re.search(r'["\']([a-zA-Z0-9_]+)["\']\s*:\s*["\']([a-zA-Z0-9_]+)["\']', ln)
            if m:
                pairs[m.group(1)] = m.group(2)
    return pairs


def _extract_func_body(js: str, brace_start: int) -> str:
    """从函数体起始 `{`（brace_start 指向该 `{`）做花括号配平，返回函数体文本。

    旧实现用固定 1400 字符窗口，会越界吃到相邻函数体，导致
    sendVideo/sendFile/sendLink（实为 _shareMediaToWechat 真实现）被误判为 companion。
    """
    depth = 0
    for i in range(brace_start, len(js)):
        c = js[i]
        if c == "{":
            depth += 1
        elif c == "}":
            depth -= 1
            if depth == 0:
                return js[brace_start:i + 1]
    return js[brace_start:]


def parse_intent_funcs() -> set:
    """只在每个函数**自身**体内判定是否为 _intentAction 占位（花括号配平，杜绝越界）。"""
    js = open(HOOK_JS, encoding="utf-8").read()
    intent = set()
    for mm in re.finditer(r"([a-zA-Z_][a-zA-Z0-9_]*)\s*:\s*function[^{]*\{", js):
        brace_start = js.rfind("{", mm.start(), mm.end())
        body = _extract_func_body(js, brace_start)
        # 仅当函数体内**唯一**返回路径是 _intentAction 才算占位；
        # 若同时存在 _shareMediaToWechat/_shareTextToWechat/_sendMessageInternal/performNow 等真实现，则不算占位。
        has_intent = "_intentAction(" in body
        has_real = any(k in body for k in (
            "_shareMediaToWechat", "_shareTextToWechat", "_sendMessageInternal",
            "_execSQLWrite", "Java.performNow", "_sendMessageBySendMsgEvent",
        ))
        if has_intent and not has_real:
            intent.add(mm.group(1))
    return intent


def latest_report() -> str:
    files = sorted(glob.glob(os.path.join(ROOT, "sdk", "tmp", "matrix_hook_catalog_*.json")),
                   key=os.path.getmtime, reverse=True)
    return files[0] if files else ""


def classify(action, passed, a2r, intent):
    rpc = a2r.get(action, "")
    if passed:
        return "✅ 已验可直调", "Frida RPC", "probe 即通（读类/无副作用）"
    if action in MONEY:
        return "🟡 需测试号", "Frida RPC + 风控", "资金动作：测试号 + 小额 + 审计，真实参数"
    if action in UI_HEAVY:
        return "🟠 需UI/u2", "u2/companion", "相机/系统UI/录音，纯 Frida 难覆盖"
    if rpc and rpc in intent:
        return "🟠 companion", "companion intent", "设备端无障碍/广播模块；无模块诚实返 false"
    if rpc:
        return "🟢 需真实参数", "Frida RPC", "probe 假参数 success:false；传真实 to_id/sns_id/msg_id 即可用"
    return "⬜ 未映射", "u2 兜底", "无 ACTION_TO_RPC；走 u2 或经 hook/execute 别名解析"


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--report", default="")
    args = ap.parse_args()
    report = args.report or latest_report()
    rep = json.load(open(report, encoding="utf-8")) if report else {"results": []}
    results = {r["action"]: bool(r.get("success")) for r in rep.get("results", [])}
    a2r = parse_action_to_rpc()
    intent = parse_intent_funcs()
    now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M")

    # 全 action 集合：catalog 结果 ∪ ACTION_TO_RPC
    all_actions = sorted(set(results) | set(a2r))
    rows = []
    for a in all_actions:
        passed = results.get(a, None)
        status, channel, cond = classify(a, passed is True, a2r, intent)
        rows.append((a, a2r.get(a, "—"), status, channel, cond))

    from collections import Counter
    dist = Counter(r[2] for r in rows)

    L = []
    L.append("---")
    L.append("tags: [工作手机, 接口, Hook, 微信, 能力矩阵, 自动生成]")
    L.append("doc-type: 验收")
    L.append("layer: 5、接口/03-Hook与微信")
    L.append('parent: "[[5、接口/03-Hook与微信/README|03-Hook与微信]]"')
    L.append('obsidian-color: "#EF6C00"')
    L.append("---\n")
    L.append("# 微信 Action 能力矩阵（诚实口径）")
    L.append("")
    L.append("> **自动生成**：`python3 sdk/scripts/gen_wechat_capability_matrix.py`  ")
    L.append(f"> **数据源**：catalog 探针 `{os.path.basename(report) or '—'}` + `ACTION_TO_RPC` + JS intent 分类 · **生成**：{now}  ")
    L.append("> **诚实口径**：catalog 用**占位参数**探针，写操作 `success:false` 属**正常**（需真实参数/上下文），非代码缺陷；本矩阵据此分级。")
    L.append("")
    L.append("## 〇、能力分级总览")
    L.append("")
    L.append("| 分级 | 含义 | 数量 |")
    L.append("|:---|:---|:---:|")
    grade_desc = {
        "✅ 已验可直调": "probe 即通，读类/无副作用，可直接调用",
        "🟢 需真实参数": "有 Frida RPC，传真实参数即可用（probe 假参数才失败）",
        "🟡 需测试号": "资金类，须测试号+小额+审计",
        "🟠 需UI/u2": "相机/系统UI/录音，纯 Frida 难覆盖，走 u2",
        "🟠 companion": "需设备端无障碍/广播模块，无模块诚实返 false",
        "⬜ 未映射": "无直接 RPC，经 hook/execute 别名解析或 u2",
    }
    for g, desc in grade_desc.items():
        L.append(f"| {g} | {desc} | {dist.get(g, 0)} |")
    L.append(f"| **合计** | | **{len(rows)}** |")
    L.append("")
    L.append("**结论**：✅+🟢 = **真实可用**（读类即调、写类传真实参数即调）；🟡🟠 需对应前置（测试号/UI/模块）；四端按本表选择调用方式，缺口走 `sdk/proxy` 或 COORD。")
    L.append("")
    L.append("## 一、动作明细")
    L.append("")
    L.append("| action | RPC | 分级 | 通道 | 可调用条件 |")
    L.append("|:---|:---|:---|:---|:---|")
    for a, rpc, status, channel, cond in rows:
        L.append(f"| `{a}` | `{rpc}` | {status} | {channel} | {cond} |")
    L.append("")
    L.append("## 二、四端用法")
    L.append("")
    L.append("- **读类（✅）**：`GET /api/v3/{module}/*` 或 `POST /api/v3/hook/execute`（hook_only:1）直接调。")
    L.append("- **写类（🟢）**：传**真实** to_id/sns_id/msg_id/group_id；经 BFF `/v1/workphone/*` 或 `hook/execute`。")
    L.append("- **资金（🟡）**：仅测试号；存客宝侧加二次确认；SDK 已入 `rate_limiter`。")
    L.append("- **UI/companion（🟠）**：需设备端 u2 在线或无障碍模块；无则诚实返回 false（不假成功）。")
    L.append("")
    L.append("## 三、关联")
    L.append("- 需求真源：`1、需求/修改/工作手机_微信全量控机与私域_20260529.md`")
    L.append("- 接口契约：`5、接口/03-Hook与微信/微信Frida_API契约.md`")
    L.append("- 四端可直调清单：`5、接口/01-规范与统一层/四端可直调接口能力清单.md`")
    L.append("- 静态对齐：`python3 sdk/scripts/wechat_interface_audit.py`（0 缺失）")
    L.append("")
    os.makedirs(os.path.dirname(OUT), exist_ok=True)
    open(OUT, "w", encoding="utf-8").write("\n".join(L))
    print(f"[ok] 写入 {OUT}")
    print(f"  动作 {len(rows)}，分级分布：{dict(dist)}")


if __name__ == "__main__":
    main()
