#!/usr/bin/env python3
"""
微信「联系客服解封」常规批量执行器

适用：以后任意设备、任意微信号被限制，编辑配置文件即可一键全自动解封。

链路：调用 SDK `POST /api/v3/account/unblock-customer-service`
    → 设备端 unblock_via_customer_service skill
    → 自动跑「安全保护中 → 联系客服 → 终止使用说明 → 联系专属客服 → 允许 → 微信安全专属客服 → AI 持续对话」

用法：
    python3 sdk/scripts/wechat_unblock_batch.py -c sdk/config/unblock_targets.yaml
    python3 sdk/scripts/wechat_unblock_batch.py -c xxx.yaml --dry-run    # 只校验不执行
    python3 sdk/scripts/wechat_unblock_batch.py -c xxx.yaml --only 192.168.110.80:5555

输出：
    sdk/tmp/unblock_batch_{ts}/
        ├── summary.md                  汇总表格
        ├── {device}_{wxid}.json        每条任务原始返回（含 turns/screenshots）
        └── {device}_{wxid}.log         每条任务日志
"""

from __future__ import annotations

import argparse
import asyncio
import json
import logging
import os
import sys
import time
import urllib.parse
import urllib.request
from dataclasses import dataclass, field
from datetime import datetime
from pathlib import Path
from typing import Any, Dict, List, Optional

LOG = logging.getLogger("unblock_batch")


# ---------- 配置加载 ----------

def _load_yaml(path: str) -> dict:
    try:
        import yaml  # type: ignore
        with open(path, "r", encoding="utf-8") as f:
            return yaml.safe_load(f) or {}
    except ImportError:
        # 退化：把简单 yaml 当 json-like 解析（仅支持本配置示例）
        import re
        content = open(path, "r", encoding="utf-8").read()
        content = re.sub(r"#.*$", "", content, flags=re.MULTILINE)
        # 极简退化：要求用户安装 PyYAML
        raise RuntimeError("未安装 PyYAML，请执行: pip install pyyaml") from None


@dataclass
class TargetTask:
    device_id: str
    wxid: str = ""
    phone: str = ""
    reason: str = ""
    max_rounds: int = 18
    chat_interval_sec: int = 8
    use_web_search: bool = True
    platform: str = "wechat"
    enabled: bool = True
    note: str = ""

    # 运行时
    started_at: float = 0
    finished_at: float = 0
    status: str = "pending"     # pending / running / success / reject / timeout / fallback / error / skipped
    final_message: str = ""
    rounds: int = 0
    raw_response: Optional[Dict[str, Any]] = field(default=None)


# ---------- 调用 SDK ----------

def _call_unblock(sdk_base: str, t: TargetTask, timeout: int) -> Dict[str, Any]:
    qs = urllib.parse.urlencode({
        "device_id": t.device_id,
        "platform": t.platform,
        "reason": t.reason,
        "phone": t.phone,
        "wxid": t.wxid,
        "max_rounds": t.max_rounds,
        "chat_interval_sec": t.chat_interval_sec,
        "use_web_search": "true" if t.use_web_search else "false",
    })
    url = f"{sdk_base.rstrip('/')}/api/v3/account/unblock-customer-service?{qs}"
    LOG.info(f"[{t.device_id}] POST {url}")
    req = urllib.request.Request(url, method="POST", headers={"Content-Type": "application/json"})
    with urllib.request.urlopen(req, timeout=timeout) as resp:
        body = resp.read().decode("utf-8")
    try:
        return json.loads(body)
    except Exception:
        return {"code": 599, "raw_body": body[:2000]}


# ---------- 单条任务 ----------

def _run_one(sdk_base: str, t: TargetTask, out_dir: Path,
             retry_on_fallback: int) -> TargetTask:
    if not t.enabled:
        t.status = "skipped"
        LOG.info(f"[{t.device_id}] disabled -> skip")
        return t

    t.started_at = time.time()
    t.status = "running"
    log_path = out_dir / f"{_safe(t.device_id)}_{_safe(t.wxid or 'noid')}.log"
    json_path = out_dir / f"{_safe(t.device_id)}_{_safe(t.wxid or 'noid')}.json"

    file_handler = logging.FileHandler(log_path, encoding="utf-8")
    file_handler.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(message)s"))
    LOG.addHandler(file_handler)

    attempts = 0
    response = None
    try:
        attempts += 1
        # 单条最长耗时：max_rounds * (chat_interval + 5) + 90 兜底
        timeout = t.max_rounds * (t.chat_interval_sec + 5) + 90
        response = _call_unblock(sdk_base, t, timeout=timeout)
        t.raw_response = response

        data = response.get("data", {}) if isinstance(response, dict) else {}
        session = data.get("session", {}) if isinstance(data, dict) else {}
        status = data.get("status") or session.get("final_status") or "unknown"
        msg = data.get("message") or data.get("error") or session.get("final_message") or ""
        rounds = session.get("rounds", 0)

        t.status = status
        t.final_message = msg[:300]
        t.rounds = int(rounds or 0)

        # 链路失败时按需重试
        while t.status == "fallback" and attempts <= retry_on_fallback:
            LOG.warning(f"[{t.device_id}] fallback，重试 {attempts}/{retry_on_fallback}")
            time.sleep(8)
            attempts += 1
            response = _call_unblock(sdk_base, t, timeout=timeout)
            t.raw_response = response
            data = response.get("data", {}) if isinstance(response, dict) else {}
            session = data.get("session", {}) if isinstance(data, dict) else {}
            t.status = data.get("status") or session.get("final_status") or "unknown"
            t.final_message = (data.get("message") or session.get("final_message") or "")[:300]
            t.rounds = int(session.get("rounds", 0) or 0)

    except Exception as exc:
        t.status = "error"
        t.final_message = f"{type(exc).__name__}: {exc}"[:300]
        LOG.exception(f"[{t.device_id}] 异常")
        response = {"error": str(exc)}
    finally:
        t.finished_at = time.time()
        try:
            json_path.write_text(
                json.dumps(response or {}, ensure_ascii=False, indent=2),
                encoding="utf-8",
            )
        except Exception:
            pass
        LOG.removeHandler(file_handler)
        file_handler.close()

    return t


def _safe(s: str) -> str:
    return "".join(c if c.isalnum() else "_" for c in s)[:48]


# ---------- 主流程 ----------

def main() -> int:
    parser = argparse.ArgumentParser(description="微信联系客服解封 · 批量执行器")
    parser.add_argument("-c", "--config", required=True, help="yaml 配置文件路径")
    parser.add_argument("--dry-run", action="store_true", help="只校验不执行")
    parser.add_argument("--only", help="只跑某个 device_id（覆盖配置）")
    parser.add_argument("--out", help="输出目录（默认 sdk/tmp/unblock_batch_{ts}）")
    parser.add_argument("--log-level", default="INFO")
    args = parser.parse_args()

    logging.basicConfig(
        level=getattr(logging, args.log_level.upper(), logging.INFO),
        format="%(asctime)s %(levelname)s %(message)s",
    )

    cfg = _load_yaml(args.config)
    sdk_base = cfg.get("sdk_base") or os.environ.get("SDK_BASE_URL") or "http://127.0.0.1:8899"
    platform = cfg.get("platform", "wechat")
    default_max = int(cfg.get("default_max_rounds", 18))
    default_interval = int(cfg.get("default_chat_interval_sec", 8))
    default_web = bool(cfg.get("default_use_web_search", True))
    concurrency = max(1, int(cfg.get("concurrency", 1)))
    retry_on_fallback = int(cfg.get("retry_on_fallback", 1))
    between_sleep = int(cfg.get("between_task_sleep_sec", 6))

    raw_targets = cfg.get("targets") or []
    tasks: List[TargetTask] = []
    for raw in raw_targets:
        if not isinstance(raw, dict):
            continue
        dev = raw.get("device_id")
        if not dev:
            continue
        if args.only and dev != args.only:
            continue
        tasks.append(TargetTask(
            device_id=dev,
            wxid=raw.get("wxid", "") or "",
            phone=raw.get("phone", "") or "",
            reason=raw.get("reason", "") or "",
            max_rounds=int(raw.get("max_rounds", default_max)),
            chat_interval_sec=int(raw.get("chat_interval_sec", default_interval)),
            use_web_search=bool(raw.get("use_web_search", default_web)),
            platform=raw.get("platform", platform),
            enabled=bool(raw.get("enabled", True)),
            note=raw.get("note", "") or "",
        ))

    if not tasks:
        LOG.error("配置中未找到任何 target")
        return 2

    ts = datetime.now().strftime("%Y%m%d_%H%M%S")
    if args.out:
        out_dir = Path(args.out)
    else:
        sdk_root = Path(__file__).resolve().parents[1]
        out_dir = sdk_root / "tmp" / f"unblock_batch_{ts}"
    out_dir.mkdir(parents=True, exist_ok=True)

    LOG.info(f"配置: {args.config}")
    LOG.info(f"SDK : {sdk_base}")
    LOG.info(f"任务数: {len(tasks)} (concurrency={concurrency})")
    LOG.info(f"输出: {out_dir}")

    if args.dry_run:
        LOG.info("--- DRY RUN ---")
        for t in tasks:
            LOG.info(f"  {t.device_id}  wxid={t.wxid}  reason={t.reason[:30]}  enabled={t.enabled}")
        return 0

    # 健康检查
    try:
        with urllib.request.urlopen(f"{sdk_base.rstrip('/')}/api/v3/connection/status", timeout=10) as resp:
            conn = json.loads(resp.read().decode())
        data = conn.get("data", {})
        LOG.info(f"SDK 健康 ws_online={data.get('online_ws_count')} adb={data.get('adb_count')}")
    except Exception as exc:
        LOG.error(f"SDK 不可达: {exc}")
        return 3

    # 执行（串行 / 并发）
    if concurrency <= 1:
        for t in tasks:
            _run_one(sdk_base, t, out_dir, retry_on_fallback)
            if between_sleep > 0:
                time.sleep(between_sleep)
    else:
        import concurrent.futures
        with concurrent.futures.ThreadPoolExecutor(max_workers=concurrency) as ex:
            futures = {ex.submit(_run_one, sdk_base, t, out_dir, retry_on_fallback): t for t in tasks}
            for fut in concurrent.futures.as_completed(futures):
                t = futures[fut]
                try:
                    fut.result()
                except Exception as exc:
                    LOG.exception(f"[{t.device_id}] 调度异常: {exc}")

    # 汇总报告
    _write_summary(tasks, out_dir, sdk_base, ts)
    success = sum(1 for t in tasks if t.status == "success")
    print(f"\n=== 完成 {success}/{len(tasks)} ===\n报告: {out_dir / 'summary.md'}")
    return 0 if success == len(tasks) else 1


def _write_summary(tasks: List[TargetTask], out_dir: Path, sdk_base: str, ts: str) -> None:
    lines = [
        f"# 微信联系客服解封 · 批量执行报告 {ts}",
        "",
        f"- SDK: `{sdk_base}`",
        f"- 任务数: {len(tasks)}",
        f"- 成功: {sum(1 for t in tasks if t.status == 'success')}",
        f"- 拒绝: {sum(1 for t in tasks if t.status == 'reject')}",
        f"- 超时: {sum(1 for t in tasks if t.status == 'timeout')}",
        f"- 链路失败: {sum(1 for t in tasks if t.status == 'fallback')}",
        f"- 异常: {sum(1 for t in tasks if t.status == 'error')}",
        f"- 跳过: {sum(1 for t in tasks if t.status == 'skipped')}",
        "",
        "| # | device | wxid | 状态 | 轮次 | 耗时(s) | 客服最终回复 |",
        "|---|--------|------|------|------|---------|--------------|",
    ]
    for i, t in enumerate(tasks, 1):
        elapsed = round(t.finished_at - t.started_at, 1) if t.started_at else 0
        msg = (t.final_message or "").replace("|", "/").replace("\n", " ")[:80]
        lines.append(f"| {i} | `{t.device_id}` | `{t.wxid}` | **{t.status}** | {t.rounds} | {elapsed} | {msg} |")
    lines += [
        "",
        "## 状态说明",
        "- **success**：客服已解封 / 已恢复使用",
        "- **reject**：客服明确拒绝解封",
        "- **timeout**：达到最大轮次或客服长时间无回复",
        "- **fallback**：未能进入客服小程序（路径变化）",
        "- **error**：调用异常（网络 / 设备不在线等）",
        "- **skipped**：配置 enabled=false",
        "",
        "## 详细 JSON",
        "",
        "每条任务详见同目录 `{device_id}_{wxid}.json`（含 turns / screenshots / web_snippets）",
    ]
    (out_dir / "summary.md").write_text("\n".join(lines), encoding="utf-8")


if __name__ == "__main__":
    sys.exit(main())
