#!/usr/bin/env python3
"""矩阵补全：Hook catalog 全 action 真机 hook/execute 探针。"""

from __future__ import annotations

import argparse
import asyncio
import json
import os
import sys
import time
from datetime import datetime
from pathlib import Path

import httpx

BASE = os.environ.get("SDK_BASE_URL", "http://127.0.0.1:8899")
DEVICE = os.environ.get("SDK_DEVICE_ID", "xgfe65eimrrofyws")
TO = "filehelper"
OUT = Path(__file__).resolve().parents[1] / "tmp"

SAFE_PARAMS: dict[str, dict] = {
    "ping": {},
    "send_message": {"to_id": TO, "content": "[catalog-probe]", "msg_type": "text"},
    "batch_send": {"user_ids": [TO], "content": "[catalog-probe]"},
    "mass_send": {"user_ids": [TO], "content": "[catalog-probe]"},
    "get_messages": {"conversation_id": TO, "limit": 3},
    "get_contacts": {"limit": 5},
    "get_contact_info": {"wxid": TO},
    "get_friend_info": {"user_id": TO},
    "search_contacts": {"keyword": "文件", "limit": 5},
    "search_contact": {"keyword": "文件", "limit": 5},
    "search_messages": {"keyword": "test", "limit": 5},
    "wechat_search": {"keyword": "微信", "limit": 5},
    "get_groups": {"limit": 5},
    "get_group_info": {"group_id": "test@chatroom"},
    "get_group_members": {"group_id": "test@chatroom"},
    "get_profile": {},
    "check_account_status": {},
    "get_favorites": {"limit": 5},
    "get_labels": {},
    "get_moments": {"limit": 3},
    "browse_channels": {"limit": 3},
    "global_search": {"keyword": "微信", "limit": 5},
    "generate_my_qr_code": {},
    "generate_group_qr_code": {"group_id": "test@chatroom"},
    "get_wallet_balance": {},
    "get_transaction_history": {"limit": 5},
    "get_wechat_steps": {},
    "get_sticker_list": {},
    "get_top_stories": {},
    "get_safety_center": {},
    "check_restrictions": {},
    "show_payment_code": {},
    "pin_chat": {"wxid": TO, "pin": False},
    "set_chat_top": {"user_id": TO, "enable": False},
    "set_do_not_disturb": {"wxid": TO, "enable": False},
    "add_favorite": {"content": "catalog-probe", "type": "text"},
    "add_to_favorites": {"content": "catalog-probe", "type": "text"},
    "set_privacy": {"setting": "moments_privacy", "value": "all"},
    "set_moments_privacy": {"privacy_type": "all", "days": 180},
    "set_moments_cover": {"image_path": "/sdcard/test.jpg"},
    "open_mini_program": {"name": "微信支付"},
    "open_miniprogram": {"name": "微信支付"},
    "send_location": {"to_id": TO, "latitude": 24.48, "longitude": 118.08, "label": "probe"},
    "send_emoji": {"to_id": TO, "emoji_name": "微笑"},
    "send_voice": {"to_id": TO, "duration": 1},
    "send_voice_message": {"to_id": TO, "duration": 1},
    "send_file": {"to_id": TO, "file_path": "/sdcard/test.txt"},
    "send_file_from_chat": {"to_id": TO, "file_path": "/sdcard/test.txt"},
    "send_image": {"to_id": TO, "image_path": "/sdcard/test.jpg"},
    "send_video": {"to_id": TO, "video_path": "/sdcard/test.mp4"},
    "send_link": {"to_id": TO, "url": "https://example.com", "title": "probe"},
    "send_card": {"to_id": TO, "card_wxid": TO},
    "send_group_message": {"group_id": "test@chatroom", "content": "probe"},
    "like_channel_video": {"index": 0},
    "like_video": {"index": 0},
    "like_wechat_steps": {"user_id": TO},
    "like_moments": {"sns_id": "probe_sns"},
    "comment_moments": {"sns_id": "probe_sns", "comment": "probe"},
    "comment_channel_video": {"video_id": "index_0", "comment": "probe"},
    "clear_cache": {},
    "check_for_update": {},
    "post_moments": {"content": "[catalog-probe]"},
    "forward_moments_link": {"content": "https://example.com probe"},
    "set_signature": {"signature": "catalog-probe"},
    "set_nickname": {"nickname": "probe"},
    "set_region": {"region": "福建 厦门"},
    "set_avatar": {"image_path": "/sdcard/test.jpg"},
    "set_remark": {"user_id": TO, "remark": "probe"},
    "set_friend_remark": {"wxid": TO, "remark": "probe"},
    "create_label": {"name": "catalog_probe"},
    "create_tag": {"tag_name": "catalog_probe"},
    "get_contacts_by_label": {"label_id": "1"},
    "get_users_by_tag": {"tag_name": "catalog_probe"},
    "add_friend": {"user_id": "28533368", "message": "catalog-probe"},
    "accept_friend": {"encrypt_username": "probe", "ticket": "probe"},
    "delete_friend": {"wxid": "invalid_wxid_probe"},
    "invite_to_group": {"group_id": "test@chatroom", "member_ids": [TO]},
    "remove_from_group": {"group_id": "test@chatroom", "member_ids": [TO]},
    "create_group": {"member_ids": [TO, TO], "group_name": "catalog-probe"},
    "set_group_announcement": {"group_id": "test@chatroom", "announcement": "probe"},
    "set_group_notice": {"group_id": "test@chatroom", "notice": "probe"},
    "set_group_name": {"group_id": "test@chatroom", "name": "probe"},
    "quit_group": {"group_id": "test@chatroom"},
    "set_contact_label": {"wxid": TO, "label_ids": []},
    "add_tag": {"wxid": TO, "tag_name": "catalog_probe"},
    "remove_tag": {"wxid": TO, "tag_name": "catalog_probe"},
    "delete_label": {"label_id": "1"},
    "delete_tag": {"label_id": "1"},
    "delete_moments": {"sns_id": "1"},
    "delete_favorite": {"local_id": "1"},
    "forward_message": {"to_id": TO, "msg_svr_id": "1779123054724"},
    "forward_multiple": {"to_id": TO, "msg_svr_ids": ["1779123054724"]},
    "recall_message": {"msg_svr_id": "1779123054724"},
    "revoke_message": {"msg_svr_id": "1779123054724"},
    "send_red_packet": {"to_id": TO, "amount": "0.01", "message": "probe"},
    "send_transfer": {"to_id": TO, "amount": "0.01", "description": "probe"},
    "transfer": {"to_id": TO, "amount": "0.01", "description": "probe"},
    "receive_red_packet": {"msg_svr_id": "1"},
    "receive_transfer": {"msg_svr_id": "1"},
    "receive_payment": {"msg_svr_id": "1"},
    "follow_channel": {"channel_id": "probe"},
    "unfollow_channel": {"channel_id": "probe"},
    "follow_official_account": {"account_id": "probe"},
    "unfollow_official_account": {"account_id": "probe"},
    "get_official_account_articles": {"account_id": "probe", "limit": 5},
    "share_channel_video": {"video_id": "index_0", "to_id": TO},
    "share_mini_program": {"app_id": "wxprobe", "to_id": TO, "title": "probe"},
    "add_friend_by_qr": {"qr_content": "probe"},
    "add_custom_emoji": {"image_path": "/sdcard/test.jpg"},
    "add_to_float": {"wxid": TO},
    "remove_from_float": {"wxid": TO},
    "clear_chat_history": {"wxid": TO},
    "clear_history": {"user_id": TO},
    "bind_phone": {"phone": "13800138000"},
    "change_password": {"old_password": "old", "new_password": "new"},
    "remove_login_device": {"device_id": "probe"},
    "register_account": {"phone": "13800138000", "nickname": "probe"},
    "login_by_password": {"phone": "13800138000", "password": "probe"},
    "login_by_sms": {"phone": "13800138000"},
    "switch_account": {"wxid": TO},
    "batch_execute": {"actions": [{"action": "getProfile", "params": {}}]},
    "get_hook_status": {},
    "get_wechat_version": {},
    "get_process_info": {},
    "voice_call": {"to_id": TO},
    "video_call": {"to_id": TO},
    "share_real_time_location": {"to_id": TO},
    "download_file": {"url": "https://example.com/probe.txt", "save_path": "/sdcard/Download/wp_probe.txt"},
    "reply_comment": {"sns_id": "probe_sns", "comment": "catalog-probe"},
    "set_what_up": {"content": "catalog-probe"},
    "logout": {},
    "get_device_info": {},
    "get_storage_info": {},
    "get_network_info": {},
    "get_recent_messages": {"limit": 5},
    "get_recent_mini_programs": {},
    "get_friend_requests": {},
    "get_official_accounts": {"limit": 5},
    "unblock_self": {},
    "unbind_phone": {},
    "enable_fingerprint": {},
    "set_account_protection": {"enable": True},
    "scan_qr_code": {},
    "set_sex": {"sex": 1},
    "set_notification": {"enable": True},
    "set_chat_background": {"wxid": TO},
    "auto_register": {},
    "check_login_state": {},
    "get_sim_phone": {},
    # ── 奥创协议补全 29 action 真机探针参数（读类空参安全 / 写类给最小 probe，真机验收才标 ✅）──
    "get_conversations": {"limit": 5},
    "get_message_by_id": {"msg_svr_id": "1779123054724"},
    "get_msg_ids_by_time": {"start_time": 0},
    "get_message_raw_xml": {"msg_svr_id": "1779123054724"},
    "add_friend_in_room": {"group_id": "test@chatroom", "wxid": TO, "verify_message": "catalog-probe"},
    "add_friend_from_phonebook": {"phone": "13800138000", "verify_message": "catalog-probe"},
    "add_friend_with_scene": {"wxid": TO, "scene": "30", "verify_message": "catalog-probe"},
    "add_friend_by_card": {"card_wxid": TO, "verify_message": "catalog-probe"},
    "join_group_by_qr": {"qr_content": "probe"},
    "approve_group_invite": {"group_id": "test@chatroom", "wxid": TO},
    "agree_join_group": {"group_id": "test@chatroom"},
    "mark_conversation_read": {"conversation_id": TO},
    "send_multi_image": {"to_id": TO, "images": ["/sdcard/test.jpg"]},
    "send_jielong": {"to_id": TO, "content": "catalog-probe 接龙"},
    "send_friend_verify": {"wxid": TO, "verify_message": "catalog-probe"},
    "voice_to_text": {"msg_svr_id": "1779123054724"},
    "get_friend_moments": {"wxid": TO},
    "get_moment_detail": {"sns_id": "probe_sns"},
    "delete_moment_comment": {"sns_id": "probe_sns", "comment_id": "1"},
    "reply_moment_comment": {"sns_id": "probe_sns", "content": "catalog-probe"},
    "stop_moments_praise": {},
    "start_nurture": {"plan": "default"},
    "stop_nurture": {},
    "detect_zombie_fans": {},
    "stop_detect_zombie_fans": {},
    "query_red_packet": {"red_packet_id": "1"},
    "get_wechat_location": {},
    "search_official_account": {"keyword": "微信"},
    "phone_action": {"op": "screenshot"},
}


def _load_actions():
    sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "app" / "agent"))
    from hook.hook_executor import ACTION_TO_RPC, ACTION_ALIASES, resolve_action, normalize_params  # type: ignore
    actions = sorted(set(ACTION_TO_RPC.keys()) | set(ACTION_ALIASES.keys()))
    return actions, resolve_action, normalize_params


async def probe_one(client: httpx.AsyncClient, device_id: str, action: str, resolve_action, normalize_params) -> dict:
    canonical = resolve_action(action)
    params = dict(SAFE_PARAMS.get(action) or SAFE_PARAMS.get(canonical, {}))
    params = normalize_params(action, params)
    t0 = time.time()
    try:
        resp = await client.post(
            f"{BASE}/api/v3/hook/execute",
            json={"device_id": device_id, "platform": "wechat", "action": action, "params": params, "hook_only": True},
            timeout=60,
        )
        body = resp.json()
        data = body.get("data") if isinstance(body.get("data"), dict) else body
        ok = bool(data.get("success")) or body.get("code") == 200 and data.get("success") is not False and "不支持" not in str(data.get("message", ""))
        if data.get("success") is False:
            ok = False
        err = "" if ok else str(data.get("message") or data.get("error") or body)[:120]
        ch = body.get("channel_used") or data.get("_channel_used") or data.get("channel") or "hook"
    except Exception as e:
        ok, err, ch = False, str(e)[:160], "error"
    # 间歇性 503 / frida_connected:False / WS 未连接 → 多次重试（frida session 繁忙或瞬断）
    retries = 0
    while not ok and retries < 3 and any(
        k in err for k in ("Frida", "frida_connected", "503", "未连接", "RPC 未成功")
    ):
        retries += 1
        await asyncio.sleep(2.0 * retries)
        try:
            resp = await client.post(
                f"{BASE}/api/v3/hook/execute",
                json={"device_id": device_id, "platform": "wechat", "action": action, "params": params, "hook_only": True},
                timeout=60,
            )
            body = resp.json()
            data = body.get("data") if isinstance(body.get("data"), dict) else body
            ok = bool(data.get("success")) or (body.get("code") == 200 and data.get("success") is not False and "不支持" not in str(data.get("message", "")))
            if data.get("success") is False:
                ok = False
            err = "" if ok else str(data.get("message") or data.get("error") or body)[:160]
            ch = body.get("channel_used") or data.get("_channel_used") or "hook"
        except Exception as e2:
            err = str(e2)[:160]
    return {"action": action, "success": ok, "channel": str(ch), "elapsed_ms": int((time.time() - t0) * 1000), "error": err, "retries": retries}


async def prefetch_context(client: httpx.AsyncClient, device_id: str) -> None:
    """从真机拉取 group_id / label_id 等，写入 SAFE_PARAMS。"""
    try:
        r = await client.post(
            f"{BASE}/api/v3/hook/execute",
            json={"device_id": device_id, "platform": "wechat", "action": "get_groups", "params": {"limit": 5}, "hook_only": True},
            timeout=60,
        )
        groups = (r.json().get("data") or {}).get("groups") or []
        gid = None
        for g in groups:
            candidate = g.get("group_id") or g.get("wxid") or g.get("username")
            if not candidate:
                continue
            chk = await client.post(
                f"{BASE}/api/v3/hook/execute",
                json={"device_id": device_id, "platform": "wechat", "action": "get_group_info", "params": {"group_id": candidate}, "hook_only": True},
                timeout=60,
            )
            if (chk.json().get("data") or {}).get("success"):
                gid = candidate
                break
        if gid:
                for k in (
                    "get_group_info", "get_group_members", "send_group_message",
                    "generate_group_qr_code", "invite_to_group", "remove_from_group",
                    "set_group_announcement", "set_group_notice", "set_group_name", "quit_group",
                ):
                    if k in SAFE_PARAMS:
                        SAFE_PARAMS[k]["group_id"] = gid
    except Exception:
        pass
    try:
        r = await client.post(
            f"{BASE}/api/v3/hook/execute",
            json={"device_id": device_id, "platform": "wechat", "action": "get_labels", "params": {}, "hook_only": True},
            timeout=60,
        )
        labels = (r.json().get("data") or {}).get("labels") or []
        if labels:
            lid = str(labels[0].get("label_id") or labels[0].get("id") or "1")
            SAFE_PARAMS["get_contacts_by_label"]["label_id"] = lid
            SAFE_PARAMS["delete_tag"]["label_id"] = lid
    except Exception:
        pass


async def main() -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument("-d", "--device-id", default=DEVICE)
    args = parser.parse_args()
    actions, resolve_action, normalize_params = _load_actions()
    results = []
    async with httpx.AsyncClient(trust_env=False) as client:
        await prefetch_context(client, args.device_id)
        for action in actions:
            row = await probe_one(client, args.device_id, action, resolve_action, normalize_params)
            results.append(row)
            print(f"{'✓' if row['success'] else '✗'} {action:<32} {row['elapsed_ms']:>5}ms{' (retry %d)' % row['retries'] if row.get('retries') else ''}")
            await asyncio.sleep(0.6)
    passed = sum(1 for x in results if x["success"])
    report = {
        "timestamp": datetime.now().isoformat(),
        "device_id": args.device_id,
        "total": len(results),
        "passed": passed,
        "failed": len(results) - passed,
        "success_rate": f"{100 * passed / max(len(results), 1):.1f}%",
        "results": results,
    }
    OUT.mkdir(parents=True, exist_ok=True)
    path = OUT / f"matrix_hook_catalog_{args.device_id}_{int(time.time())}.json"
    path.write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
    print(f"\nHook catalog: {passed}/{len(results)} → {path}")
    return 0 if passed == len(results) else 1


if __name__ == "__main__":
    raise SystemExit(asyncio.run(main()))
