#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Hermes Agent 全自動配置工具 v2.0
=================================

自動處理以下場景：
  1. 全新空配置（model/providers 都不存在）
  2. 部分配置（model 有，providers 空）
  3. YAML 語法錯誤（冒號後缺空格）
  4. UTF-8 BOM 導致解析失敗
  5. .env 完全缺失
  6. Windows / Linux / macOS 跨平台

支援 OpenAI 兼容 API (含 EavaryTech 自建)

用法 (PowerShell):
    python setup_hermes_v2.py --key "sk-..." 
    $env:HERMES_EAVARY_KEY="sk-..." ; python setup_hermes_v2.py
    python setup_hermes_v2.py   # 互動輸入（隱藏）
"""

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

# ============================================================
#  配置（CLI 參數覆蓋）
# ============================================================
DEFAULT_URL   = "https://llm-api-ai.eavarytech.com/v1"
DEFAULT_MODEL = "Chat"
CONTEXT_LEN   = 32768


class HermesDoctor:
    def __init__(self, api_key, base_url, model):
        self.api_key = api_key
        self.base_url = base_url
        self.model = model
        self.os_type = platform.system()
        
        # Hermes 路徑
        if self.os_type == "Windows":
            self.hermes_dir = Path(os.environ.get("USERPROFILE", str(Path.home()))) / ".hermes"
        else:
            self.hermes_dir = Path.home() / ".hermes"
        
        self.config_path = self.hermes_dir / "config.yaml"
        self.env_path = self.hermes_dir / ".env"
        self.backup_dir = self.hermes_dir / "backups"
        
        # 診斷結果
        self.findings = []   # (severity, category, message)
        self.fixes_applied = []
        
        # 配置狀態
        self.state = {
            "hermes_installed": False,
            "config_exists": False,
            "has_bom": False,
            "yaml_errors": [],
            "model_empty": True,
            "providers_empty": True,
            "api_key_in_providers": False,
            "openai_section_present": False,
            "env_exists": False,
            "env_has_key": False,
            "needs_fix": False,
            "config_content": "",
        }

    def log(self, severity, category, msg):
        self.findings.append((severity, category, msg))

    def run_cmd(self, cmd, timeout=30):
        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)

    # ================= 階段 1: 環境檢測 =================
    def phase1_environment(self):
        self.section("Phase 1: 環境檢測")
        
        self.print(f"系統: {self.os_type} {platform.release()}")
        self.print(f"Python: {sys.version.split()[0]}")
        self.print(f"Hermes 目錄: {self.hermes_dir}")
        print()

        # 1.1 檢測 hermes CLI
        rc, out, _ = self.run_cmd(["hermes", "--version"], timeout=10)
        if rc == 0:
            self.state["hermes_installed"] = True
            self.success(f"hermes 已安裝: {(out or '').strip()}")
        else:
            self.error("hermes CLI 未找到")
            self.print("安裝方法:")
            if self.os_type == "Windows":
                self.print("   iwr -useb https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.ps1 | iex")
            else:
                self.print("   curl -fsSL https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.sh | bash")
            return False
        
        # 1.2 確保目錄
        self.hermes_dir.mkdir(parents=True, exist_ok=True)
        self.backup_dir.mkdir(exist_ok=True)
        self.success(f"目錄就緒")
        
        # 1.3 檢測 config.yaml 存在性
        if not self.config_path.exists():
            self.warn("config.yaml 不存在 → 將建立新文件")
            self.state["needs_fix"] = True
            return True
        
        self.state["config_exists"] = True
        size = self.config_path.stat().st_size
        self.success(f"config.yaml 存在 ({size} bytes)")
        return True

    # ================= 階段 2: 內容分析 =================
    def phase2_analyze(self):
        if not self.state["config_exists"]:
            return
            
        self.section("Phase 2: 配置分析")
        
        # 2.1 讀取內容（處理 BOM）
        try:
            raw_bytes = self.config_path.read_bytes()
            if raw_bytes.startswith(b'\xef\xbb\xbf'):
                self.state["has_bom"] = True
                self.warn("UTF-8 BOM 檢測到 → Hermes 無法解析")
                self.state["needs_fix"] = True
                content = raw_bytes[3:].decode("utf-8")
            else:
                content = raw_bytes.decode("utf-8")
        except UnicodeDecodeError as e:
            self.error(f"config.yaml 編碼錯誤: {e}")
            return
        except Exception as e:
            self.error(f"無法讀取 config.yaml: {e}")
            return
        
        self.state["config_content"] = content
        
        # 2.2 YAML 語法檢測
        yaml_errors = self.check_yaml(content)
        if yaml_errors:
            self.state["yaml_errors"] = yaml_errors
            self.error(f"YAML 語法錯誤 {len(yaml_errors)} 處:")
            for err in yaml_errors[:5]:
                self.print(f"   - {err}")
            if len(yaml_errors) > 5:
                self.print(f"   ... 還有 {len(yaml_errors) - 5} 處")
            self.state["needs_fix"] = True
        else:
            self.success("YAML 語法正確")
        
        # 2.3 model 段
        model_empty_match = re.search(r"^model:\s*['\"]?\s*['\"]?\s*$", content, re.MULTILINE)
        has_model = "model:" in content
        if model_empty_match:
            self.state["model_empty"] = True
            self.warn("model 段為空字符串")
            self.state["needs_fix"] = True
        elif not has_model:
            self.state["model_empty"] = True
            self.warn("model 段缺失")
            self.state["needs_fix"] = True
        else:
            self.state["model_empty"] = False
            self.success("model 段已存在")
            # 檢查內容
            m = re.search(r"^model:.*?\n((?:\s+\S.*?\n)+)", content, re.MULTILINE)
            if m:
                model_block = m.group(0)
                if "default:" in model_block and "provider:" in model_block:
                    default_m = re.search(r"default:\s*(\S+)", model_block)
                    provider_m = re.search(r"provider:\s*(\S+)", model_block)
                    if default_m and provider_m:
                        self.print(f"   default={default_m.group(1)}, provider={provider_m.group(1)}")
        
        # 2.4 providers 段
        providers_empty_match = re.search(r"^providers:\s*\{\}\s*$", content, re.MULTILINE)
        has_providers = "providers:" in content
        has_openai = re.search(r"^providers:\s*\n\s+openai:", content, re.MULTILINE)
        
        if providers_empty_match:
            self.state["providers_empty"] = True
            self.warn("providers 是空字典 {}")
            self.state["needs_fix"] = True
        elif not has_providers:
            self.state["providers_empty"] = True
            self.warn("providers 段缺失")
            self.state["needs_fix"] = True
        elif has_openai:
            self.state["providers_empty"] = False
            self.state["openai_section_present"] = True
            
            # 檢查 api_key
            if re.search(r'providers:\s*\n\s+openai:\s*\n(?:\s+\S.*\n)*?\s+api_key:\s*"sk-', content):
                self.state["api_key_in_providers"] = True
                self.success("providers.openai.api_key 已配置")
            elif re.search(r'api_key:\s*[\'"]?sk-', content):
                self.state["api_key_in_providers"] = True
                self.success("providers 中找到 api_key (sk- 開頭)")
            else:
                self.warn("providers.openai 存在但 api_key 未設定或格式不對")
                self.state["needs_fix"] = True
        else:
            self.warn("providers 段存在但無 openai 配置")
            self.state["needs_fix"] = True
        
        # 2.5 base_url 格式
        url_match = re.search(r'base_url:\s*[\'"]?([^\'"\s]+)', content)
        if url_match:
            url = url_match.group(1)
            if url.endswith("/v1"):
                self.success(f"base_url: {url}")
            else:
                self.warn(f"base_url 應以 /v1 結尾: {url}")
                self.state["needs_fix"] = True
        
        # 2.6 .env 檢測
        if self.env_path.exists():
            self.state["env_exists"] = True
            try:
                env_content = self.env_path.read_text(encoding="utf-8")
                if re.search(r'^(?:OPENAI_API_KEY\s*=\s*[\'"]?sk-)|(?:HERMES_EAVARY_KEY\s*=\s*[\'"]?sk-)', env_content, re.MULTILINE):
                    self.state["env_has_key"] = True
                    self.success(".env 包含 API key")
                else:
                    self.print("   .env 存在但無 API key")
            except Exception as e:
                self.warn(f"無法讀取 .env: {e}")
        else:
            self.print("   .env 不存在（可選，將自動建立）")

    def check_yaml(self, content):
        """檢測常見 YAML 錯誤（冒號後缺空格）"""
        errors = []
        lines = content.split("\n")
        for i, line in enumerate(lines, 1):
            stripped = line.rstrip()
            if not stripped or stripped.startswith("#"):
                continue
            # 跳過字符串內部冒號
            if re.match(r'^[\s]*[a-zA-Z_][a-zA-Z0-9_-]*:[^"\s/:]', stripped):
                errors.append(f"line {i}: '{stripped[:80]}'")
        return errors

    # ================= 階段 3: 自動修補 =================
    def phase3_fix(self):
        if not self.state["needs_fix"]:
            self.section("Phase 3: 配置已就緒，無需修補")
            return
        
        self.section("Phase 3: 自動修補")
        
        # 備份
        ts = datetime.now().strftime("%Y%m%d_%H%M%S")
        if self.state["config_exists"]:
            backup = self.backup_dir / f"config.yaml.bak.{ts}"
            shutil.copy2(self.config_path, backup)
            self.print(f"[+] 備份: {backup}")
        
        content = self.state["config_content"]
        
        # Fix 1: 移除 BOM (已經 read_bytes 處理過，這裡 content 已乾淨)
        if self.state["has_bom"]:
            self.print(f"[+] 移除 BOM（將以 utf-8 無 BOM 寫入）")
        
        # Fix 2: model 段
        new_model = (
            f"model:\n"
            f"  default: {self.model}\n"
            f"  provider: openai\n"
            f"  context_length: {CONTEXT_LEN}\n"
            f"  temperature: 0.7\n"
            f"  max_tokens: 4096"
        )
        
        if self.state["model_empty"]:
            if re.search(r"^model:\s*['\"]?\s*['\"]?\s*$", content, re.MULTILINE):
                content = re.sub(
                    r"^model:\s*['\"]?\s*['\"]?\s*$",
                    new_model, content, count=1, flags=re.MULTILINE,
                )
                self.print("[+] 替換空 model 段")
            elif "model:" not in content:
                content = new_model + "\n\n" + content
                self.print("[+] 在文件頭添加 model 段")
            else:
                self.warn("model 段存在但空，保留原樣（將由 providers 段解決）")
        
        # Fix 3: providers 段
        new_providers = (
            f"providers:\n"
            f"  openai:\n"
            f'    api_key: "{self.api_key}"\n'
            f"    base_url: {self.base_url}\n"
            f"    default_model: {self.model}\n"
            f"    timeout: 60"
        )
        
        if self.state["providers_empty"]:
            if re.search(r"^providers:\s*\{\}\s*$", content, re.MULTILINE):
                content = re.sub(
                    r"^providers:\s*\{\}\s*$",
                    new_providers, content, count=1, flags=re.MULTILINE,
                )
                self.print("[+] 替換空 providers 段")
            elif "providers:" not in content:
                content += "\n" + new_providers + "\n"
                self.print("[+] 在文件末尾追加 providers 段")
        elif not self.state["api_key_in_providers"] and self.state["openai_section_present"]:
            # 有 providers.openai 但沒 api_key → 注入
            content = re.sub(
                r"^(providers:\s*\n\s+openai:\s*\n)",
                f"    api_key: \"sk-{self.api_key}\""
                f"    base_url: {self.base_url}\n"
                "    default_model: Chat\n"
                "    timeout: 60\n",
                content, count=1, flags=re.MULTILINE,
            )
            self.print("[+] 在 providers.openai 注入 api_key")
        
        # Fix 4: .env 確保有 key（保險）
        if not self.state["env_has_key"]:
            self.write_env()
            self.print("[+] 寫入 .env 作為備援")
        
        # 寫回
        self.config_path.write_text(content, encoding="utf-8")
        self.print(f"[+] 寫入 config.yaml: {self.config_path}")
        self.fixes_applied.append("config.yaml")

    def write_env(self):
        lines = []
        if self.env_path.exists():
            try:
                lines = self.env_path.read_text(encoding="utf-8").split("\n")
            except Exception:
                lines = []
        
        # 清掉舊的 OPENAI_API_KEY / HERMES_EAVARY_KEY / OPENAI_API_BASE_URL
        keep = []
        for l in lines:
            s = l.strip()
            if re.match(r'^(OPENAI_API_KEY|HERMES_EAVARY_KEY|OPENAI_API_BASE_URL|OPENAI_EAVARY_BASE_URL)\s*=', s):
                continue
            if s:
                keep.append(l)
        
        keep.append(f"OPENAI_API_KEY={self.api_key}")
        keep.append(f"OPENAI_API_BASE_URL={self.base_url}")
        keep.append(f"HERMES_EAVARY_KEY={self.api_key}")
        
        self.env_path.write_text("\n".join(keep) + "\n", encoding="utf-8")

    # ================= 階段 4: 重啟 + 驗證 =================
    def phase4_verify(self):
        self.section("Phase 4: 重啟 + 驗證")
        
        # 停止 gateway
        self.print("[i] 停止 gateway...")
        self.run_cmd(["hermes", "gateway", "stop"], timeout=15)
        
        # 啟動 gateway
        self.print("[i] 啟動 gateway...")
        rc, out, err = self.run_cmd(["hermes", "gateway", "start"], timeout=30)
        if out.strip():
            self.print(f"    {out.strip()[:200]}")
        
        # doctor
        self.print("[i] hermes doctor...")
        rc, out, err = self.run_cmd(["hermes", "doctor"], timeout=60)
        doctor_ok = False
        if out.strip():
            for ln in out.split("\n"):
                if ln.strip():
                    self.print(f"    {ln}")
            if "✓" in out or "PASS" in out.upper() or rc == 0:
                doctor_ok = True
        
        # chat 測試
        self.print("[i] hermes chat -q '回 PONG'...")
        rc, out, err = self.run_cmd(["hermes", "chat", "-q", "回 PONG"], timeout=90)
        output = (out + err).strip()
        self.print(f"    {output[:400]}")
        
        if "PONG" in output or "pong" in output.lower():
            self.section("[+] 配置成功！")
            return True
        elif "No inference provider" in output:
            self.error("仍報錯 → 貼輸出給我")
            return False
        else:
            self.warn("其他錯誤 → 貼輸出給我")
            return False

    # ================= 工具方法 =================
    def section(self, t):
        print(f"\n-- {t} {'-' * max(0, 50 - len(t))}")

    def print(self, m):
        print(f"   {m}")

    def success(self, m):
        print(f"   [+] {m}")

    def warn(self, m):
        print(f"   [!] {m}")

    def error(self, m):
        print(f"   [x] {m}")

    # ================= 主流程 =================
    def run(self):
        print("=" * 60)
        print("  Hermes Agent 全自動配置 v2.0")
        print("=" * 60)
        
        if not self.phase1_environment():
            return False
        self.phase2_analyze()
        self.phase3_fix()
        # 即使不需要修補，也跑驗證
        return self.phase4_verify()


def get_api_key():
    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument("--key", default=None)
    parser.add_argument("--url", default=DEFAULT_URL)
    parser.add_argument("--model", default=DEFAULT_MODEL)
    parser.add_argument("--help", "-h", action="store_true")
    parser.add_argument("--diagnose-only", action="store_true")
    args, _ = parser.parse_known_args()

    if args.help:
        print(__doc__)
        sys.exit(0)

    api_key = args.key
    if not api_key:
        api_key = os.environ.get("HERMES_EAVARY_KEY", "").strip()
    if not api_key:
        try:
            import getpass
            print()
            api_key = getpass.getpass("請輸入 EavaryTech API key (隱藏): ").strip()
        except Exception:
            print()
            api_key = input("請輸入 API key: ").strip()

    if not api_key:
        print("錯誤: 未提供 API key")
        sys.exit(1)

    return api_key, args.url, args.model, args.diagnose_only


def main():
    api_key, base_url, model, diagnose_only = get_api_key()
    
    doctor = HermesDoctor(api_key, base_url, model)
    if diagnose_only:
        doctor.phase1_environment()
        doctor.phase2_analyze()
        sys.exit(0)
    
    success = doctor.run()
    sys.exit(0 if success else 1)


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