#!/usr/bin/env python3
"""isep-doctor — 這台機器上每一個 Claude Code，到底有沒有把 ISEP 載進來（inkstone/ISEP#150）

為什麼要有這支：
  plugin 載入失敗時，ISEP 的閘**一支都不會跑**，而且**沒有任何閘能講出來**——
  講話的 isep-presence-beacon 本身就在那個沒載入的 plugin 裡。

為什麼一台機器要分開問（09-13 總管複驗 c6991 推翻了第一版）：
  同一台 Mac 上有**不只一個** Claude Code：CLI（例：/opt/homebrew/bin/claude 2.1.220）
  與 Claude Desktop 內建的那一份（~/Library/Application Support/Claude/claude-code/<版本>/…，例：2.1.266）。
  它們讀同一份 ~/.claude，**卻不一定得出同一個結論**：
    settings.json 的 extraKnownMarketplaces.inkstone.source 多了 path 時——
      2.1.266：整個 marketplace 被忽略 ⇒ isep@inkstone 報 "Marketplace inkstone not found"
      2.1.220：照常載入
  第一版只看「設定長得像會出事」就對整台機器判 ❌，在 2.1.220 上是假陽性。

判準（實際狀態，不是設定長相）：
  對每一個找得到的 Claude Code 各跑一次 `plugin list --json`，看它**自己**回報的 isep@inkstone：
    沒有這一筆      ⇒ ❌ 沒安裝
    帶 errors      ⇒ ❌ 沒載入（照實轉述；若 settings 比已安裝的 marketplace 多了抓取欄位，附上原因與修法）
    沒有 errors    ⇒ ✅（即使設定裡有 path——那台實際載得進來就是載得進來）
    跑不動／看不懂 ⇒ ◐ 查不了（讀不到不等於沒載入，也不等於有）
  設定比對**只當解釋**，永遠不單獨判 ❌。

離開碼：0 全部 ✅｜1 至少一個 ❌｜2 沒有 ❌、但至少一個查不了（或一個 Claude Code 都找不到）

測試用環境變數：
  ISEP_DOCTOR_CLAUDE_BINS  用這份清單取代自動尋找；以 os.pathsep 分隔，每項 `名稱=可執行檔路徑`
  ISEP_DOCTOR_CLAUDE_HOME  代替 ~/.claude
迴歸測試：scripts/test-isep-doctor.sh（離線）／scripts/test-isep-doctor-live.sh（真的 Claude Code、隔離 HOME）
"""
import glob
import json
import os
import re
import shutil
import subprocess
import sys

PLUGIN_ID = "isep@inkstone"
MARKET = "inkstone"
FETCH_FIELDS = ("path", "ref", "headers", "sparsePaths")
DESKTOP_GLOB = "~/Library/Application Support/Claude/claude-code/*/claude.app/Contents/MacOS/claude"

home = os.environ.get("ISEP_DOCTOR_CLAUDE_HOME") or os.path.expanduser("~/.claude")


def _ver_key(path):
    m = re.search(r"/claude-code/([0-9][0-9.]*)/", path)
    return tuple(int(x) for x in m.group(1).split(".") if x) if m else ()


def find_claudes():
    """回傳 [(名稱, 路徑)]。Desktop 只取最新版本那一份（它啟動的就是那一份；舊資料夾可能殘留）。"""
    env = os.environ.get("ISEP_DOCTOR_CLAUDE_BINS")
    if env is not None:
        out = []
        for item in env.split(os.pathsep):
            if "=" in item:
                name, path = item.split("=", 1)
                out.append((name, path))
        return out
    out = []
    cli = shutil.which("claude")
    if cli:
        out.append(("CLI", cli))
    desk = sorted(glob.glob(os.path.expanduser(DESKTOP_GLOB)), key=_ver_key)  # 數字排序：2.1.99 < 2.1.266
    if desk:
        out.append(("Desktop", desk[-1]))
    return out


def version_of(exe):
    try:
        r = subprocess.run([exe, "--version"], capture_output=True, text=True, timeout=30)
        return (r.stdout.strip().split() or ["?"])[0]
    except Exception:  # noqa: BLE001
        return "?"


def ask(exe):
    """回傳 (狀態, 內容)。狀態：'list' 內容＝plugin list 陣列；'err' 內容＝查不了的原因。"""
    try:
        r = subprocess.run([exe, "plugin", "list", "--json"], capture_output=True, text=True, timeout=90)
    except Exception as e:  # noqa: BLE001
        return "err", f"執行失敗（{e}）"
    try:
        data = json.loads(r.stdout)
        if not isinstance(data, list):
            raise ValueError("不是陣列")
        return "list", data
    except Exception:  # noqa: BLE001
        tail = (r.stdout or r.stderr or "").strip().splitlines()[-1:] or ["（沒有輸出）"]
        return "err", f"plugin list --json 看不懂（exit={r.returncode}：{tail[0][:200]}）"


def load(p):
    try:
        return json.load(open(p))
    except Exception:  # noqa: BLE001
        return {}


declared = (load(os.path.join(home, "settings.json")).get("extraKnownMarketplaces") or {}).get(MARKET, {}).get("source")
known = (load(os.path.join(home, "plugins", "known_marketplaces.json")).get(MARKET) or {}).get("source")
extra = []
if isinstance(declared, dict):
    extra = [k for k in FETCH_FIELDS if k in declared and (not isinstance(known, dict) or declared.get(k) != known.get(k))]

claudes = find_claudes()
if not claudes:
    print("◐ 查不了：這台找不到任何 Claude Code 可執行檔（CLI 與 Desktop 都沒有）")
    sys.exit(2)

bad, unknown, extra_hit = [], [], False
for name, exe in claudes:
    label = f"{name} {version_of(exe)}"
    state, data = ask(exe)
    if state == "err":
        unknown.append(name)
        print(f"◐ {label}：查不了——{data}（讀不到不等於沒載入）")
        print(f"   {exe}")
        continue
    entry = next((p for p in data if p.get("id") == PLUGIN_ID), None)
    if entry is None:
        bad.append(name)
        print(f"❌ {label}：{PLUGIN_ID} 沒有安裝——用這個 Claude Code 開的 session 一支 ISEP 閘都沒有")
        print("   修法：claude plugin marketplace add https://git.uncle6.me/inkstone/ISEP.git && claude plugin install isep@inkstone")
        continue
    errors = entry.get("errors") or []
    if not errors:
        print(f"✅ {label}：{PLUGIN_ID} {entry.get('version')} 已載入（enabled={entry.get('enabled')}）")
        continue
    bad.append(name)
    print(f"❌ {label}：{PLUGIN_ID} {entry.get('version')} 沒有載入——用這個 Claude Code 開的 session 一支 ISEP 閘都沒有")
    for e in errors:
        print(f"   Claude Code 回報：{e}")
    if extra:
        extra_hit = True
        print(f"   可能原因：~/.claude/settings.json 的 extraKnownMarketplaces.{MARKET}.source 多了 {', '.join(extra)}，"
              "這一版把它當 git 抓取欄位，跟已安裝的 marketplace 對不上，整個 marketplace 被忽略")

if extra_hit:
    fields = ", ".join(repr(k) for k in extra)
    print("修法（一行，只拿掉上面點名的欄位；改完完全關掉 Claude Desktop／重開 CLI session 再跑一次本指令）：")
    print(f"   python3 -c \"import json,os;p=os.path.expanduser('~/.claude/settings.json');d=json.load(open(p));"
          f"s=d['extraKnownMarketplaces']['{MARKET}']['source'];[s.pop(k,None) for k in ({fields},)];"
          "json.dump(d,open(p,'w'),ensure_ascii=False,indent=2)\"")

if bad:
    sys.exit(1)
sys.exit(2 if unknown else 0)
