#!/usr/bin/env python3.11
"""
n8n Workflow 備份腳本 - 使用 n8n MCP API
路徑: /Users/accusys/momentry_core_0.1/monitor/workflow/backup_n8n_mcp.py

功能:
  - 使用 n8n MCP API 導出所有 workflows
  - 按用戶/Tags 分組備份
  - 變更偵測
  - SHA256 校驗

前置需求:
  pip3.11 install mcp

使用方式:
  python3.11 backup_n8n_mcp.py              # 備份所有 workflows
  python3.11 backup_n8n_mcp.py --tags prod # 只備份有 prod tag 的 workflow
  python3.11 backup_n8n_mcp.py --diff       # 只顯示變更
  python3.11 backup_n8n_mcp.py --list       # 列出可用備份
  python3.11 backup_n8n_mcp.py --audit      # 產生安全審計報告
"""

import json
import os
import sys
import argparse
import hashlib
from datetime import datetime
from pathlib import Path

# 嘗試載入 MCP SDK
try:
    from mcp import ClientSession, StdioServerParameters
    from mcp.client.stdio import stdio_client

    MCP_AVAILABLE = True
except ImportError:
    MCP_AVAILABLE = False
    print("Warning: MCP SDK not available. Install with: pip install mcp")

# ============================================
# 配置
# ============================================
BACKUP_ROOT = Path("/Users/accusys/momentry/backup/n8n_workflows/mcp")
LOG_DIR = Path("/Users/accusys/momentry/log/monitor")
TIMESTAMP = datetime.now().strftime("%Y%m%d_%H%M%S")

# 顏色
RED = "\033[0;31m"
GREEN = "\033[0;32m"
YELLOW = "\033[1;33m"
BLUE = "\033[0;34m"
NC = "\033[0m"

# ============================================
# 日誌
# ============================================
LOG_FILE = LOG_DIR / "workflow_backup_mcp.log"


def log(msg: str, color: str = ""):
    timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    full_msg = f"[{timestamp}] {msg}"
    if color:
        print(f"{color}{full_msg}{NC}")
    else:
        print(full_msg)
    LOG_FILE.parent.mkdir(parents=True, exist_ok=True)
    with open(LOG_FILE, "a") as f:
        f.write(full_msg + "\n")


def log_info(msg: str):
    log(msg)


def log_success(msg: str):
    log(f"✅ {msg}", GREEN)


def log_error(msg: str):
    log(f"❌ {msg}", RED)


def log_warn(msg: str):
    log(f"⚠️  {msg}", YELLOW)


# ============================================
# MCP 客戶端
# ============================================
class N8nMCPClient:
    def __init__(self):
        self.session = None
        self.tools = {}

    async def connect(self):
        if not MCP_AVAILABLE:
            raise RuntimeError("MCP SDK not available")

        server_params = StdioServerParameters(
            command="/opt/homebrew/bin/mcp-n8n",
            env={
                "N8N_BASE_URL": "https://n8n.momentry.ddns.net",
                "N8N_API_KEY": os.environ.get("N8N_API_KEY", ""),
            },
        )

        async with stdio_client(server_params) as (read, write):
            self.session = ClientSession(read, write)
            await self.session.initialize()

            # 獲取可用工具列表
            tools_result = await self.session.list_tools()
            for tool in tools_result.tools:
                self.tools[tool.name] = tool

            log_info(f"Connected to n8n MCP, available tools: {len(self.tools)}")

    async def call_tool(self, tool_name: str, arguments: dict = None):
        if not self.session:
            raise RuntimeError("Not connected")

        result = await self.session.call_tool(tool_name, arguments or {})
        return result.content

    async def list_workflows(
        self, tags: list = None, active: bool = None, limit: int = 100
    ):
        """列出 workflows"""
        args = {"limit": limit}
        if tags:
            args["tags"] = tags
        if active is not None:
            args["active"] = active

        content = await self.call_tool("n8n_list_workflows", args)
        data = json.loads(content[0].text)
        return data.get("data", [])

    async def get_workflow(self, workflow_id: str):
        """取得 workflow 詳情"""
        content = await self.call_tool("n8n_get_workflow", {"workflowId": workflow_id})
        data = json.loads(content[0].text)
        return data

    async def list_tags(self):
        """列出所有 tags"""
        content = await self.call_tool("n8n_list_tags")
        data = json.loads(content[0].text)
        return data.get("data", [])

    async def get_workflow_tags(self, workflow_id: str):
        """取得 workflow 的 tags"""
        content = await self.call_tool(
            "n8n_get_workflow_tags", {"workflowId": workflow_id}
        )
        data = json.loads(content[0].text)
        return data

    async def list_executions(self, workflow_id: str = None, limit: int = 50):
        """列出執行記錄"""
        args = {"limit": limit}
        if workflow_id:
            args["workflowId"] = workflow_id

        content = await self.call_tool("n8n_list_executions", args)
        data = json.loads(content[0].text)
        return data.get("data", [])

    async def generate_audit(self):
        """產生安全審計報告"""
        content = await self.call_tool("n8n_generate_audit", {})
        data = json.loads(content[0].text)
        return data

    async def health_check(self):
        """健康檢查"""
        content = await self.call_tool("n8n_health_check", {})
        return content[0].text if content else "Unknown"


# ============================================
# 備份功能
# ============================================
def compute_sha256(data: dict) -> str:
    """計算 JSON 的 SHA256"""
    json_str = json.dumps(data, sort_keys=True, ensure_ascii=False)
    return hashlib.sha256(json_str.encode()).hexdigest()


def save_backup(workflows: list, tags: list, backup_dir: Path, metadata: dict = None):
    """保存備份到磁盤"""
    backup_dir.mkdir(parents=True, exist_ok=True)

    # 主要備份文件
    backup_file = backup_dir / "workflows.json"
    with open(backup_file, "w", encoding="utf-8") as f:
        json.dump(workflows, f, indent=2, ensure_ascii=False)

    # SHA256 校驗
    sha256_hash = compute_sha256(workflows)
    sha256_file = backup_dir / "workflows.json.sha256"
    with open(sha256_file, "w") as f:
        f.write(sha256_hash)

    # Tags 文件
    if tags:
        tags_file = backup_dir / "tags.json"
        with open(tags_file, "w", encoding="utf-8") as f:
            json.dump(tags, f, indent=2, ensure_ascii=False)

    # 元數據文件
    meta = metadata or {}
    meta.update(
        {
            "timestamp": TIMESTAMP,
            "workflow_count": len(workflows),
            "sha256": sha256_hash,
        }
    )
    meta_file = backup_dir / "manifest.json"
    with open(meta_file, "w", encoding="utf-8") as f:
        json.dump(meta, f, indent=2, ensure_ascii=False)

    log_success(f"Backup saved: {backup_file} ({len(workflows)} workflows)")
    return backup_dir


def load_latest_backup() -> tuple:
    """載入最新備份"""
    if not BACKUP_ROOT.exists():
        return None, None, None

    # 找到最新的備份目錄
    backup_dirs = sorted([d for d in BACKUP_ROOT.iterdir() if d.is_dir()], reverse=True)
    if not backup_dirs:
        return None, None, None

    latest = backup_dirs[0]
    workflows_file = latest / "workflows.json"
    tags_file = latest / "tags.json"
    manifest_file = latest / "manifest.json"

    workflows = None
    tags = None
    manifest = None

    if workflows_file.exists():
        with open(workflows_file) as f:
            workflows = json.load(f)

    if tags_file.exists():
        with open(tags_file) as f:
            tags = json.load(f)

    if manifest_file.exists():
        with open(manifest_file) as f:
            manifest = json.load(f)

    return workflows, tags, manifest


def detect_changes(current: list, previous: list) -> dict:
    """偵測 workflow 變更"""
    current_map = {wf["id"]: wf for wf in current}
    previous_map = {wf["id"]: wf for wf in previous} if previous else {}

    changes = {
        "added": [],
        "modified": [],
        "deleted": [],
    }

    # 新增或修改
    for wf_id, wf in current_map.items():
        if wf_id not in previous_map:
            changes["added"].append(wf["name"])
        else:
            current_hash = compute_sha256(wf)
            previous_hash = compute_sha256(previous_map[wf_id])
            if current_hash != previous_hash:
                changes["modified"].append(wf["name"])

    # 刪除
    for wf_id in previous_map:
        if wf_id not in current_map:
            changes["deleted"].append(previous_map[wf_id]["name"])

    return changes


def print_changes(changes: dict):
    """打印變更摘要"""
    print(f"\n{BLUE}{'=' * 50}{NC}")
    print(f"{BLUE}n8n Workflow 變更偵測{NC}")
    print(f"{BLUE}{'=' * 50}{NC}\n")

    if changes["added"]:
        print(f"{GREEN}+ 新增 ({len(changes['added'])}):{NC}")
        for name in changes["added"]:
            print(f"  - {name}")
        print()

    if changes["modified"]:
        print(f"{YELLOW}~ 修改 ({len(changes['modified'])}):{NC}")
        for name in changes["modified"]:
            print(f"  - {name}")
        print()

    if changes["deleted"]:
        print(f"{RED}- 刪除 ({len(changes['deleted'])}):{NC}")
        for name in changes["deleted"]:
            print(f"  - {name}")
        print()

    if not any(changes.values()):
        print(f"{GREEN}✅ 無變更{NC}\n")


# ============================================
# 主程式
# ============================================
async def main():
    parser = argparse.ArgumentParser(description="n8n Workflow 備份 (MCP API)")
    parser.add_argument("--tags", nargs="*", help="只備份有特定 tags 的 workflows")
    parser.add_argument(
        "--active-only", action="store_true", help="只備份啟用的 workflows"
    )
    parser.add_argument("--diff", action="store_true", help="只顯示變更，不備份")
    parser.add_argument("--list", action="store_true", help="列出可用備份")
    parser.add_argument("--audit", action="store_true", help="產生安全審計報告")
    parser.add_argument("--executions", action="store_true", help="同時備份執行記錄")
    parser.add_argument("--dry-run", action="store_true", help="測試模式，不實際備份")
    args = parser.parse_args()

    log_info("=" * 50)
    log_info("n8n Workflow 備份 (MCP API)")
    log_info("=" * 50)

    # 列出可用備份
    if args.list:
        print(f"\n{BLUE}可用備份:{NC}\n")
        if BACKUP_ROOT.exists():
            for d in sorted(BACKUP_ROOT.iterdir(), reverse=True)[:10]:
                manifest_file = d / "manifest.json"
                if manifest_file.exists():
                    with open(manifest_file) as f:
                        meta = json.load(f)
                    print(f"  {d.name} - {meta.get('workflow_count', 0)} workflows")
        else:
            print("  無可用備份")
        print()
        return

    # 連接 MCP
    client = N8nMCPClient()
    try:
        await client.connect()
    except Exception as e:
        log_error(f"連接失敗: {e}")
        sys.exit(1)

    # 健康檢查
    try:
        health = await client.health_check()
        log_info(f"健康狀態: {health}")
    except Exception as e:
        log_warn(f"健康檢查失敗: {e}")

    # 安全審計
    if args.audit:
        log_info("產生安全審計報告...")
        try:
            audit = await client.generate_audit()
            audit_dir = BACKUP_ROOT / TIMESTAMP / "audit"
            audit_dir.mkdir(parents=True, exist_ok=True)
            with open(audit_dir / "audit.json", "w") as f:
                json.dump(audit, f, indent=2)
            log_success("審計報告已保存")
        except Exception as e:
            log_error(f"審計失敗: {e}")
        return

    # 獲取 workflows
    log_info("獲取 workflows...")
    try:
        workflows = await client.list_workflows(
            tags=args.tags, active=True if args.active_only else None
        )
        log_info(f"找到 {len(workflows)} workflows")
    except Exception as e:
        log_error(f"獲取 workflows 失敗: {e}")
        sys.exit(1)

    # 獲取 tags
    log_info("獲取 tags...")
    try:
        tags = await client.list_tags()
        log_info(f"找到 {len(tags)} tags")
    except Exception as e:
        log_warn(f"獲取 tags 失敗: {e}")
        tags = []

    # 變更偵測
    log_info("檢查變更...")
    previous_wf, _, _ = load_latest_backup()
    changes = detect_changes(workflows, previous_wf)

    if args.diff:
        print_changes(changes)
        return

    # 顯示變更摘要
    total_changes = sum(len(v) for v in changes.values())
    if total_changes > 0:
        print_changes(changes)
        log_warn(f"發現 {total_changes} 個變更")
    else:
        log_info("✅ 無變更")

    # 測試模式
    if args.dry_run:
        log_info("測試模式結束")
        return

    # 執行備份
    backup_dir = BACKUP_ROOT / TIMESTAMP
    metadata = {
        "filter_tags": args.tags,
        "active_only": args.active_only,
        "changes": changes if total_changes > 0 else None,
    }

    save_backup(workflows, tags, backup_dir, metadata)

    # 執行記錄備份
    if args.executions:
        log_info("備份執行記錄...")
        try:
            executions_data = []
            for wf in workflows[:10]:  # 只備份前 10 個 workflow
                execs = await client.list_executions(wf["id"], limit=20)
                executions_data.append(
                    {
                        "workflow_id": wf["id"],
                        "workflow_name": wf["name"],
                        "executions": execs[:10],  # 每個只取 10 筆
                    }
                )

            exec_dir = backup_dir / "executions"
            exec_dir.mkdir(exist_ok=True)
            with open(exec_dir / "executions.json", "w") as f:
                json.dump(executions_data, f, indent=2)
            log_success(f"執行記錄已保存: {len(executions_data)} workflows")
        except Exception as e:
            log_warn(f"執行記錄備份失敗: {e}")

    # 清理舊備份（保留 30 天）
    log_info("清理舊備份...")
    try:
        cutoff = datetime.now().timestamp() - (30 * 24 * 60 * 60)
        for d in BACKUP_ROOT.iterdir():
            if d.is_dir() and d.stat().st_mtime < cutoff:
                import shutil

                shutil.rmtree(d)
                log_info(f"已刪除舊備份: {d.name}")
    except Exception as e:
        log_warn(f"清理失敗: {e}")

    log_success("備份完成!")
    print(f"\n備份位置: {backup_dir}")
    print(f"SHA256: {compute_sha256(workflows)}")


if __name__ == "__main__":
    import asyncio

    asyncio.run(main())
