#!/usr/bin/env python3
"""
帅府 · 兵端最小连通测试脚本（跨平台：Windows / macOS / Linux）

用途：在另一台设备上证明能跟主帅府的 D1 沙盘通信。
依赖：Python 3.8+，Node + npx（用来调 wrangler 访问 D1）。

跑法：
  1) 第一次：装 Node + npx wrangler login（同账号 lf4911453@gmail.com）
       浏览器会弹一次 OAuth 授权，按一下 Allow 就完事
  2) python bing_check.py --bing win-test

它会做 5 件事：
  - 上报心跳到 D1 bing_heartbeat
  - 拉自己的 dispatched 任务
  - 把第一条改成 running
  - 模拟干活（sleep 3s）
  - 标 done + 写 last_progress

跑完后回主 mac 这边能看到任务从 dispatched → running → done。
"""
from __future__ import annotations

import argparse
import json
import os
import platform
import shutil
import socket
import subprocess
import sys
import time
from pathlib import Path

DB_NAME = "shuaifu_sandtable"


def _find_npx() -> str:
    """找 npx 路径。Windows 是 npx.cmd；Mac/Linux 是 npx。"""
    for candidate in ("npx", "npx.cmd"):
        p = shutil.which(candidate)
        if p:
            return p
    # nvm 兜底（macOS / Linux）
    nvm = Path.home() / ".nvm/versions/node"
    if nvm.exists():
        for v in sorted(nvm.iterdir(), reverse=True):
            cand = v / "bin/npx"
            if cand.exists():
                return str(cand)
    sys.exit("✗ 找不到 npx，请装 Node.js（https://nodejs.org/）")


NPX = _find_npx()


def d1_run(sql: str) -> dict:
    """通过 wrangler 跑 D1 SQL，返回 JSON 解析结果。"""
    cmd = [NPX, "--yes", "wrangler", "d1", "execute", DB_NAME,
           "--remote", "--json", "--command", sql]
    # Windows 下 npx.cmd 必须 shell=False + 显式找路径
    r = subprocess.run(cmd, capture_output=True, text=True, timeout=60,
                       stdin=subprocess.DEVNULL)
    if r.returncode != 0:
        out = r.stderr.strip() or r.stdout.strip()
        # 排查提示
        if "not authenticated" in out.lower() or "wrangler login" in out.lower():
            sys.exit("✗ wrangler 没登录。先跑一次：npx wrangler login（同账号）")
        sys.exit(f"✗ wrangler rc={r.returncode}: {out[:400]}")
    out = r.stdout.strip()
    # wrangler --json 偶尔混 banner，找首个 [
    for i, ch in enumerate(out):
        if ch in "[{":
            try:
                return json.loads(out[i:])
            except json.JSONDecodeError:
                pass
    sys.exit(f"✗ wrangler 输出非 JSON: {out[:300]}")


def query(sql: str) -> list[dict]:
    data = d1_run(sql)
    if isinstance(data, list) and data:
        return data[0].get("results", []) or []
    return []


def execute(sql: str) -> dict:
    data = d1_run(sql)
    if isinstance(data, list) and data:
        return data[0].get("meta", {})
    return {}


def _esc(s: str) -> str:
    return str(s).replace("'", "''")


def heartbeat(bing_id: str, state: str = "idle", task_id: str = "") -> None:
    machine = bing_id.split(":", 1)[0]
    task_part = f"'{_esc(task_id)}'" if task_id else "NULL"
    execute(f"""
        INSERT INTO bing_heartbeat (bing_id, machine_name, state, current_task_id, last_heartbeat)
        VALUES ('{_esc(bing_id)}', '{_esc(machine)}', '{_esc(state)}', {task_part}, datetime('now'))
        ON CONFLICT(bing_id) DO UPDATE SET
            machine_name=excluded.machine_name,
            state=excluded.state,
            current_task_id=excluded.current_task_id,
            last_heartbeat=datetime('now')
    """)


def fetch_pending(bing_id: str) -> list[dict]:
    return query(
        f"SELECT id, title, description, complexity, business_line "
        f"FROM tasks WHERE state='dispatched' AND assigned_bing='{_esc(bing_id)}' "
        f"ORDER BY priority DESC LIMIT 5"
    )


def mark_running(task_id: str, note: str = "") -> None:
    execute(
        f"UPDATE tasks SET state='running', last_progress='{_esc(note)}', "
        f"updated_at=datetime('now') WHERE id='{_esc(task_id)}'"
    )


def mark_done(task_id: str, note: str = "") -> None:
    execute(
        f"UPDATE tasks SET state='done', last_progress='{_esc(note)}', "
        f"updated_at=datetime('now') WHERE id='{_esc(task_id)}'"
    )


def main():
    p = argparse.ArgumentParser(description="帅府兵端连通测试")
    p.add_argument("--bing", default="win-test", help="本机 bing_id")
    p.add_argument("--no-execute", action="store_true",
                   help="只拉任务不改状态（dry run）")
    args = p.parse_args()

    bing = args.bing
    print(f"━━━ 帅府兵端测试 ━━━")
    print(f"本机:   {platform.system()} {platform.release()}  ({socket.gethostname()})")
    print(f"bing_id: {bing}")
    print(f"npx:    {NPX}")
    print()

    # 1. 心跳
    print("[1/5] 上报心跳...", end=" ", flush=True)
    heartbeat(bing, state="idle")
    print("✓")

    # 2. 拉任务
    print("[2/5] 查我的待办...", end=" ", flush=True)
    pending = fetch_pending(bing)
    print(f"✓ {len(pending)} 条")
    for t in pending:
        print(f"      · [{t['complexity']}] {t['id']}  {t['title']}")
        if t.get("description"):
            print(f"        说明: {t['description'][:100]}")

    if not pending:
        print()
        print("没有派给我的任务。如果你期望有任务但拿不到：")
        print(f"  1) bing_id 是不是对的？(--bing 参数 或主帅府 config.yaml bings[0].id)")
        print(f"  2) 主帅府那边任务的 assigned_bing 字段是不是 '{bing}'")
        return

    if args.no_execute:
        print()
        print("(--no-execute，未改状态)")
        return

    target = pending[0]

    # 3. 改 running
    print(f"[3/5] {target['id']} → running...", end=" ", flush=True)
    mark_running(target["id"], note=f"win-test 接单 @ {time.strftime('%H:%M:%S')}")
    heartbeat(bing, state="working", task_id=target["id"])
    print("✓")

    # 4. 假装干活
    print("[4/5] 模拟干活 3 秒...", end=" ", flush=True)
    time.sleep(3)
    print("✓")

    # 5. 改 done
    print(f"[5/5] {target['id']} → done...", end=" ", flush=True)
    mark_done(target["id"], note=f"完工 @ {time.strftime('%H:%M:%S')} from {socket.gethostname()}")
    heartbeat(bing, state="idle")
    print("✓")

    print()
    print("━━━ 完成 ━━━")
    print(f"任务 {target['id']} 已标 done。")
    print("回主帅府那边跑 `python main.py status` 或看飞书表确认状态变化。")


if __name__ == "__main__":
    main()
