#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Hermes Agent EavaryTech API 配置工具 v3
========================================
完全自動化:
  1. 探測 ~/.hermes/config.yaml 狀態
  2. 自動修補 6 種常見問題（空 model/空 providers/YAML 語法/BOM/.env 缺失/跨平台）
  3. 重啟 gateway + doctor + chat 驗證

用法 (PowerShell):
    python setup_hermes_v3.py --key "sk-SFt...Ufg"
    python setup_hermes_v3.py              # 互動輸入 key
    $env:HERMES_EAVARY_KEY="sk-..." ; python setup_hermes_v3.py
"""

import os
import sys
import re
import shutil
import subprocess
import argparse
import platform
from pathlib import Path
from datetime import datetime

# ============================================================
#  內建預設值（可用命令行 --key / 環境變量覆蓋）
# ============================================================
DEFAULT_API_KEY = "sk-SFt...iUfg"
DEFAULT_BASE_URL = "https://llm-api-ai.eavarytech.com/v1"
DEFAULT_MODEL = "Chat"
ALT_MODEL = "Reasoning"

# 觸發下載的副檔名（YAML 段自動修補會填值）
ATTACH_EXTS = (".py", ".sh")


# ============================================================
#  工具函數
# ============================================================
def banner():
    print()
    print("=" * 60)
    print("  Hermes Agent 全自動配置 v3")
    print("=" * 60)
    print()


def phase(n, total, name):
    print(f"\n[Phase {n}/{total}] {name}")
    print("-" * 60)


def step(msg):
    print(f"   [i] {msg}")


def ok(msg):
    print(f"   [+] {msg}")


def warn(msg):
    print(f"   [!] {msg}")


def err(msg):
    print(f"   [x] {msg}")


def run(cmd, timeout=30):
    """執行命令，返回 (rc, stdout, stderr)"""
    try:
        r = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout, encoding="utf-8")
        return r.returncode, r.stdout, r.stderr
    except FileNotFoundError:
        return -1, "", f"{cmd[0]} not found"
    except subprocess.TimeoutExpired:
        return -2, "", "timeout"
    except Exception as e:
        return -3, "", str(e)


def confirm(prompt, default_yes=True):
    """Y/n 確認"""
    suffix = "[Y/n]" if default_yes else "[y/N]"
    try:
        ans = input(f"   {prompt} {suffix}: ").strip().lower()
    except EOFError:
        return default_yes
    if not ans:
        return default_yes
    return ans in ("y", "yes")


def get_hermes_dir():
    if os.name == "nt":
        return Path(os.environ.get("USERPROFILE", str(Path.home()))) / ".hermes"
    return Path.home() / ".hermes"


def get_api_key(args):
    if args.key:
        return args.key.strip()
    env = os.environ.get("HERMES_EAVARY_KEY", "").strip()
    if env:
        return env
    if DEFAULT_API_KEY:
        print()
        print(f"   使用內建 DEFAULT_API_KEY (前 6 字: {DEFAULT_API_KEY[:6]}...)")
        return DEFAULT_API_KEY
    try:
        import getpass
        print()
        return getpass.getpass("   請輸入 API key: ").strip()
    except Exception:
        return input("   請輸入 API key: ").strip()


# ============================================================
#  診斷
# ============================================================
def diagnose(config_path):
    findings = []  # [(severity, message)]
    issues = []    # 修補列隊

    if not config_path.exists():
        findings.append(("WARN", f"{config_path} 不存在 → 將建立新檔"))
        issues.append("create_new")
        return findings, issues, ""

    # 檢測 BOM
    raw = config_path.read_bytes()
    has_bom = raw[:3] == b"\xef\xbb\xbf"
    if has_bom:
        findings.append(("ERROR", "UTF-8 BOM 字符會導致 YAML 解析失敗 → 移除"))
        issues.append("strip_bom")

    content = raw.decode("utf-8-sig" if has_bom else "utf-8")
    if has_bom:
        findings.append(("INFO", "已自動去除 BOM"))

    # 檢測 YAML 語法（冒號後缺空格）
    bad_lines = []
    for i, line in enumerate(content.split("\n"), 1):
        s = line.strip()
        if not s or s.startswith("#"):
            continue
        if re.match(r"^[a-zA-Z_][a-zA-Z0-9_]*:[^/\"'\s\n#]", s):
            bad_lines.append((i, s[:80]))
    if bad_lines:
        findings.append(("ERROR",
            f"YAML 冒號後缺空格 ({len(bad_lines)} 處) → 自動修補"))
        for ln, txt in bad_lines[:5]:
            findings.append(("WARN", f"  line {ln}: {txt}"))
        issues.append("fix_yaml_spaces")

    # 檢測空 model
    if re.search(r"^model:\s*['\"]?\s*['\"]?\s*$", content, re.MULTILINE):
        findings.append(("WARN", "model 段為空 → 自動填入"))
        issues.append("fix_model")
    elif "model:" not in content:
        findings.append(("WARN", "model 段缺失 → 自動添加"))
        issues.append("fix_model")

    # 檢測空 providers
    if re.search(r"^providers:\s*\{\}\s*$", content, re.MULTILINE):
        findings.append(("WARN", "providers 為空字典 → 自動填入"))
        issues.append("fix_providers")
    elif "providers:" not in content:
        findings.append(("WARN", "providers 段缺失 → 自動添加"))
        issues.append("fix_providers")

    # 檢測 base_url 格式
    url_match = re.search(r'base_url:\s*["\']?([^"\'\s\n]+)', content)
    if url_match and not url_match.group(1).endswith("/v1"):
        findings.append(("WARN", f"base_url 應以 /v1 結尾: {url_match.group(1)}"))

    return findings, issues, content


# ============================================================
#  修補
# ============================================================
def backup_file(path, backup_dir):
    backup_dir.mkdir(parents=True, exist_ok=True)
    ts = datetime.now().strftime("%Y%m%d_%H%M%S")
    target = backup_dir / f"{path.name}.bak.{ts}"
    shutil.copy2(path, target)
    return target


def fix_yaml_spaces(content):
    """修補冒號後缺空格"""
    def repl(m):
        return f"{m.group(1)}: {m.group(2)}"
    return re.sub(r"^(\s*[a-zA-Z_][a-zA-Z0-9_]*):(\S)", repl, content, flags=re.MULTILINE)


def build_model_block(model):
    return (
        "model:\n"
        f"  default: {model}\n"
        "  provider: openai\n"
        "  context_length: 32768\n"
        "  temperature: 0.7\n"
        "  max_tokens: 4096"
    )


def build_providers_block(api_key, base_url, model):
    return (
        "providers:\n"
        "  openai:\n"
        f'    api_key: "{api_key}"\n'
        f"    base_url: {base_url}\n"
        f"    default_model: {model}\n"
        "    timeout: 60"
    )


def write_env(env_path, api_key, base_url):
    env_lines = []
    if env_path.exists():
        env_lines = [
            l for l in env_path.read_text(encoding="utf-8").split("\n")
            if not re.match(r"^(OPENAI_API_KEY|OPENAI_EAVARY_BASE_URL|OPENAI_API_BASE_URL)\s*=", l.strip())
            and l.strip()
        ]
    env_lines.append(f"OPENAI_API_KEY={api_key}")
    env_lines.append(f"OPENAI_EAVARY_BASE_URL={base_url}")
    env_lines.append(f"OPENAI_API_BASE_URL={base_url}")
    env_path.write_text("\n".join(env_lines) + "\n", encoding="utf-8")


def apply_fixes(content, issues, api_key, base_url, model):
    if not issues:
        return content, []

    applied = []
    changed = False

    # 修 BOM（內容已透過讀取時剝除，這裡不用動）
    if "strip_bom" in issues:
        applied.append("移除 BOM")
        changed = True

    # 修 yaml 空格
    if "fix_yaml_spaces" in issues:
        new_content = fix_yaml_spaces(content)
        if new_content != content:
            content = new_content
            changed = True
            applied.append("修補 YAML 冒號空格")

    # 修 model 段
    if "fix_model" in issues:
        block = build_model_block(model)
        if re.search(r"^model:\s*['\"]?\s*['\"]?\s*$", content, re.MULTILINE):
            content = re.sub(r"^model:\s*['\"]?\s*['\"]?\s*$", block, content, count=1, flags=re.MULTILINE)
        else:
            if content.strip():
                content = content + "\n\n" + block + "\n"
            else:
                content = block + "\n"
        changed = True
        applied.append("填入 model 段")

    # 修 providers 段
    if "fix_providers" in issues:
        block = build_providers_block(api_key, base_url, model)
        if re.search(r"^providers:\s*\{\}\s*$", content, re.MULTILINE):
            content = re.sub(r"^providers:\s*\{\}\s*$", block, content, count=1, flags=re.MULTILINE)
        elif re.search(r"^providers:\s*$", content, re.MULTILINE):
            content = re.sub(r"^providers:\s*$", block, content, count=1, flags=re.MULTILINE)
        else:
            if content.strip():
                content = content + "\n\n" + block + "\n"
            else:
                content = content + block + "\n"
        changed = True
        applied.append("填入 providers.openai 段")

    return content, applied


# ============================================================
#  主流程
# ============================================================
def main():
    banner()

    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument("--key", default=None)
    parser.add_argument("--url", default=DEFAULT_BASE_URL)
    parser.add_argument("--model", default=DEFAULT_MODEL)
    parser.add_argument("--no-fix", action="store_true", help="只診斷不修")
    parser.add_argument("--no-restart", action="store_true", help="跳過重啟")
    parser.add_argument("--help", "-h", action="store_true")
    args, _ = parser.parse_known_args()

    if args.help:
        print(__doc__)
        return 0

    api_key = get_api_key(args)
    if not api_key:
        err("未提供 API key")
        return 1
    ok(f"API key 長度: {len(api_key)} chars")

    hermes_dir = get_hermes_dir()
    config_path = hermes_dir / "config.yaml"
    env_path = hermes_dir / ".env"
    backup_dir = hermes_dir / "backups"

    step(f"系統: {platform.system()} {platform.release()}")
    step(f"Python: {sys.version.split()[0]}")
    step(f"Hermes home: {hermes_dir}")
    step(f"config: {config_path}")
    step(f".env: {env_path}")

    # 探測 hermes 安裝
    rc, out, _ = run(["hermes", "--version"], timeout=10)
    if rc != 0:
        err("hermes CLI 未安裝")
        print("       安裝: iwr -useb https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.ps1 | iex")
        return 1
    ok(f"hermes: {(out or '').strip()}")

    # ── Phase 1: 診斷
    phase(1, 5, "診斷現有 config")
    findings, issues, content = diagnose(config_path)
    for sev, msg in findings:
        if sev == "ERROR":   err(msg)
        elif sev == "WARN":  warn(msg)
        elif sev == "INFO":  step(msg)
        else:                ok(msg)

    if not issues:
        ok("config.yaml 狀態良好，無需修補")

    if args.no_fix:
        ok("--no-fix 模式，跳過修補和驗證")
        return 0

    # ── Phase 2: 備份
    phase(2, 5, "備份")
    hermes_dir.mkdir(parents=True, exist_ok=True)
    backup_dir.mkdir(exist_ok=True)
    backup_target = None
    if config_path.exists():
        backup_target = backup_file(config_path, backup_dir)
        ok(f"備份: {backup_target}")
    else:
        ok("config.yaml 不存在，跳過備份")

    # ── Phase 3: 修補
    phase(3, 5, "修補 config.yaml")
    original = content
    new_content, applied = apply_fixes(content, issues, api_key, args.url, args.model)

    for action in applied:
        ok(action)

    if applied:
        config_path.write_text(new_content, encoding="utf-8")
        ok(f"寫入: {config_path}")
    else:
        step("無內容變更")

    # 寫 .env (Hermes 文檔推薦的備援方案)
    phase(4, 5, "寫入 .env")
    write_env(env_path, api_key, args.url)
    ok(f"寫入: {env_path}")
    ok("OPENAI_API_KEY + OPENAI_EAVARY_BASE_URL 設定完成")

    # 顯示修改後
    print()
    print("   --- config.yaml 前 40 行 ---")
    for ln in new_content.split("\n")[:40]:
        print(f"   | {ln}")
    print("   " + "-" * 56)
    print()

    # ── Phase 5: 重啟 + 驗證
    if args.no_restart:
        warn("--no-restart，跳過重啟")
        return 0

    phase(5, 5, "重啟 gateway + 驗證")
    step("停止 gateway ...")
    run(["hermes", "gateway", "stop"], timeout=15)

    step("啟動 gateway ...")
    rc, out, errmsg = run(["hermes", "gateway", "start"], timeout=30)
    if out.strip():
        print("       " + out.strip().replace("\n", "\n       "))
    if errmsg.strip() and "y/n" not in errmsg.lower():
        print("       " + errmsg.strip().replace("\n", "\n       "))

    step("hermes doctor ...")
    rc, out, errmsg = run(["hermes", "doctor"], timeout=60)
    if out.strip():
        for ln in out.split("\n"):
            if ln.strip():
                print("       " + ln)
    if errmsg.strip():
        for ln in errmsg.split("\n"):
            if ln.strip():
                print("       " + ln)

    step("hermes chat -q '回 PONG' ...")
    rc, out, errmsg = run(["hermes", "chat", "-q", "回 PONG"], timeout=90)
    output = out + errmsg
    print("       " + output.strip().replace("\n", "\n       "))

    print()
    if "PONG" in output or "pong" in output.lower():
        ok("成功！chat 已通 EavaryTech API")
        print()
        step("用法提示：")
        print("       /model Chat          切到 Qwen")
        print("       /model Reasoning     切到 DeepSeek V4 Flash")
        print("       /model               查看當前")
        return 0
    if "No inference provider" in output:
        err("仍報 No inference provider")
        step("貼上面 doctor + chat 輸出給我")
        return 1
    warn("無法判斷成功與否")
    return 0


if __name__ == "__main__":
    try:
        sys.exit(main())
    except KeyboardInterrupt:
        print("\n   [x] 用戶中斷")
        sys.exit(1)
    except Exception as e:
        err(f"未預期錯誤: {e}")
        import traceback
        traceback.print_exc()
        sys.exit(1)
