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

功能:
  - 使用 n8n REST API 導出所有 workflows
  - 按用戶/Tags 分組備份
  - 變更偵測
  - 差異備份（只備份變更的 workflow）
  - SHA256 校驗
  - 備份驗證

前置需求:
  pip3.11 install requests

使用方式:
  python3.11 backup_n8n_api.py              # 備份所有 workflows
  python3.11 backup_n8n_api.py --diff        # 只顯示變更
  python3.11 backup_n8n_api.py --incremental # 差異備份（只備份變更的）
  python3.11 backup_n8n_api.py --list        # 列出可用備份
  python3.11 backup_n8n_api.py --verify      # 驗證最新備份
  python3.11 backup_n8n_api.py --stats      # 顯示備份統計
"""

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

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

N8N_BASE_URL = os.environ.get("N8N_BASE_URL", "https://n8n.momentry.ddns.net")
N8N_API_KEY = os.environ.get("N8N_API_KEY", "")

# 顏色
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_api.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)


# ============================================
# n8n API 客戶端
# ============================================
class N8nAPIClient:
    def __init__(self, base_url: str, api_key: str):
        self.base_url = base_url.rstrip("/")
        self.session = requests.Session()
        self.session.headers.update({"X-N8N-API-Key": api_key})
        self.session.verify = True

    def _request(self, method: str, path: str, **kwargs):
        url = f"{self.base_url}/api/v1{path}"
        resp = self.session.request(method, url, timeout=30, **kwargs)
        resp.raise_for_status()
        return resp.json()

    def list_workflows(self, limit: int = 100, cursor: str = None) -> dict:
        """列出 workflows"""
        params = {"limit": limit}
        if cursor:
            params["cursor"] = cursor
        return self._request("GET", "/workflows", params=params)

    def get_workflow(self, workflow_id: str) -> dict:
        """取得 workflow 詳情"""
        return self._request("GET", f"/workflows/{workflow_id}")

    def list_tags(self) -> dict:
        """列出所有 tags"""
        return self._request("GET", "/tags")

    def get_workflow_tags(self, workflow_id: str) -> dict:
        """取得 workflow 的 tags"""
        return self._request("GET", f"/workflows/{workflow_id}/tags")

    def list_executions(
        self, workflow_id: str = None, limit: int = 50, status: str = None
    ) -> dict:
        """列出執行記錄"""
        params: dict = {"limit": limit}
        if workflow_id:
            params["workflowId"] = workflow_id
        if status:
            params["status"] = status
        return self._request("GET", "/executions", params=params)

    def health_check(self) -> bool:
        """健康檢查"""
        try:
            self._request("GET", "/workflows", params={"limit": 1})
            return True
        except Exception:
            return False


# ============================================
# 備份功能
# ============================================
def compute_sha256(data) -> 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, tags, 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 = dict(metadata) if metadata else {}
    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]
    return load_backup(latest)


def load_backup(backup_dir: Path) -> tuple:
    """載入指定備份"""
    workflows = None
    tags = None
    manifest = None

    workflows_file = backup_dir / "workflows.json"
    tags_file = backup_dir / "tags.json"
    manifest_file = backup_dir / "manifest.json"

    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 list_backups() -> list:
    """列出所有可用備份"""
    if not BACKUP_ROOT.exists():
        return []

    backups = []
    for d in sorted(BACKUP_ROOT.iterdir(), reverse=True):
        if d.is_dir():
            manifest_file = d / "manifest.json"
            if manifest_file.exists():
                with open(manifest_file) as f:
                    meta = json.load(f)
                backups.append(
                    {
                        "path": d,
                        "name": d.name,
                        "timestamp": meta.get("timestamp"),
                        "workflow_count": meta.get("workflow_count", 0),
                        "sha256": meta.get("sha256"),
                    }
                )
    return backups


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")


def get_changed_workflows(current: list, previous: list) -> list:
    """取得變更的 workflows"""
    if not previous:
        return current

    current_map = {wf["id"]: wf for wf in current}
    previous_map = {wf["id"]: wf for wf in previous}

    changed = []
    for wf in current:
        wf_id = wf["id"]
        if wf_id not in previous_map:
            changed.append(wf)
        else:
            current_hash = compute_sha256(wf)
            previous_hash = compute_sha256(previous_map[wf_id])
            if current_hash != previous_hash:
                changed.append(wf)

    return changed


def verify_backup(backup_dir: Path) -> dict:
    """驗證備份完整性"""
    result = {
        "valid": True,
        "errors": [],
        "workflow_count": 0,
        "sha256_match": False,
    }

    workflows_file = backup_dir / "workflows.json"
    sha256_file = backup_dir / "workflows.json.sha256"
    manifest_file = backup_dir / "manifest.json"

    if not workflows_file.exists():
        result["valid"] = False
        result["errors"].append("workflows.json 不存在")
        return result

    if not sha256_file.exists():
        result["valid"] = False
        result["errors"].append("workflows.json.sha256 不存在")
        return result

    if not manifest_file.exists():
        result["valid"] = False
        result["errors"].append("manifest.json 不存在")
        return result

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

    with open(sha256_file) as f:
        stored_hash = f.read().strip()

    current_hash = compute_sha256(workflows)
    result["sha256_match"] = current_hash == stored_hash
    result["workflow_count"] = len(workflows)

    if not result["sha256_match"]:
        result["valid"] = False
        result["errors"].append(
            f"SHA256 不匹配: 預期 {stored_hash}, 實際 {current_hash}"
        )

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

    expected_count = manifest.get("workflow_count", 0)
    if expected_count != len(workflows):
        result["valid"] = False
        result["errors"].append(
            f"數量不匹配: manifest={expected_count}, 實際={len(workflows)}"
        )

    return result


def print_backup_stats():
    """顯示備份統計"""
    backups = list_backups()

    if not backups:
        print(f"\n{BLUE}沒有可用備份{NC}\n")
        return

    print(f"\n{BLUE}{'=' * 60}{NC}")
    print(f"{BLUE}n8n Workflow 備份統計{NC}")
    print(f"{BLUE}{'=' * 60}{NC}\n")

    total_workflows = sum(b["workflow_count"] for b in backups)
    print(f"  備份數量: {len(backups)}")
    print(f"  總 workflows: {total_workflows}")
    print(f"  最新備份: {backups[0]['name'] if backups else 'N/A'}")
    print()

    print(f"{'時間戳':<25} {'Workflows':<12} {'SHA256 前 16 字元'}")
    print("-" * 60)
    for b in backups[:10]:
        ts = b.get("timestamp", "N/A") or "N/A"
        count = b.get("workflow_count", 0)
        sha = (b.get("sha256", "") or "")[:16]
        print(f"{ts:<25} {count:<12} {sha}")


# ============================================
# 主程式
# ============================================
def main():
    parser = argparse.ArgumentParser(description="n8n Workflow 備份 (REST API)")
    parser.add_argument(
        "--active-only", action="store_true", help="只備份啟用的 workflows"
    )
    parser.add_argument("--diff", action="store_true", help="只顯示變更，不備份")
    parser.add_argument(
        "--incremental", action="store_true", help="差異備份（只備份變更的）"
    )
    parser.add_argument("--list", action="store_true", help="列出可用備份")
    parser.add_argument("--verify", action="store_true", help="驗證最新備份")
    parser.add_argument("--stats", action="store_true", help="顯示備份統計")
    parser.add_argument(
        "--failed-only", action="store_true", help="只備份失敗的執行記錄"
    )
    parser.add_argument("--dry-run", action="store_true", help="測試模式，不實際備份")
    args = parser.parse_args()

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

    # 顯示統計
    if args.stats:
        print_backup_stats()
        return

    # 驗證備份
    if args.verify:
        backups = list_backups()
        if not backups:
            log_error("沒有可用備份")
            sys.exit(1)

        latest = backups[0]["path"]
        log_info(f"驗證備份: {latest.name}")
        result = verify_backup(latest)

        if result["valid"]:
            log_success(f"備份有效: {result['workflow_count']} workflows, SHA256 匹配")
        else:
            log_error(f"備份無效: {result['errors']}")
            sys.exit(1)
        return

    # 列出可用備份
    if args.list:
        print(f"\n{BLUE}可用備份:{NC}\n")
        backups = list_backups()
        if backups:
            for b in backups[:10]:
                ts = b.get("timestamp", "") or ""
                count = b.get("workflow_count", 0)
                print(f"  {b['name']} - {count} workflows ({ts})")
        else:
            print("  無可用備份")
        print()
        return

    # 創建 API 客戶端
    if not N8N_API_KEY:
        log_error("N8N_API_KEY 環境變數未設定")
        sys.exit(1)

    client = N8nAPIClient(N8N_BASE_URL, N8N_API_KEY)

    # 健康檢查
    try:
        if client.health_check():
            log_success("n8n API 連線正常")
        else:
            log_error("n8n API 連線失敗")
            sys.exit(1)
    except Exception as e:
        log_error(f"連線檢查失敗: {e}")
        sys.exit(1)

    # 獲取 workflows
    log_info("獲取 workflows...")
    try:
        all_workflows = []
        cursor = None
        while True:
            result = client.list_workflows(limit=100, cursor=cursor)
            all_workflows.extend(result.get("data", []))
            cursor = result.get("nextCursor")
            if not cursor:
                break

        # 過濾
        if args.active_only:
            all_workflows = [wf for wf in all_workflows if wf.get("active")]

        log_info(f"找到 {len(all_workflows)} workflows")
    except Exception as e:
        log_error(f"獲取 workflows 失敗: {e}")
        sys.exit(1)

    # 獲取 tags
    log_info("獲取 tags...")
    try:
        tags_result = client.list_tags()
        tags = tags_result.get("data", [])
        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(all_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

    # 差異備份
    if args.incremental:
        if not previous_wf:
            log_warn("沒有舊備份，執行完整備份")
            backup_workflows = all_workflows
        else:
            backup_workflows = get_changed_workflows(all_workflows, previous_wf)
            if not backup_workflows:
                log_info("✅ 沒有變更，跳過備份")
                return
            log_info(f"差異備份: {len(backup_workflows)} workflows")
    else:
        backup_workflows = all_workflows

    # 執行備份
    backup_dir = BACKUP_ROOT / TIMESTAMP
    metadata = {
        "active_only": args.active_only,
        "incremental": args.incremental,
        "changes": changes if total_changes > 0 else None,
        "backup_count": len(backup_workflows),
        "total_count": len(all_workflows),
    }

    save_backup(backup_workflows, tags, backup_dir, metadata)

    # 執行記錄備份
    if args.failed_only:
        log_info("備份失敗的執行記錄...")
        try:
            executions_data = []
            for wf in backup_workflows[:10]:
                try:
                    execs_result = client.list_executions(
                        wf["id"], limit=20, status="failed"
                    )
                    if execs_result.get("data"):
                        executions_data.append(
                            {
                                "workflow_id": wf["id"],
                                "workflow_name": wf["name"],
                                "failed_executions": len(execs_result.get("data", [])),
                                "executions": execs_result.get("data", []),
                            }
                        )
                except Exception:
                    pass

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

    # 清理舊備份（保留 30 天）
    log_info("清理舊備份...")
    try:
        import shutil

        cutoff = datetime.now().timestamp() - (30 * 24 * 60 * 60)
        for d in BACKUP_ROOT.iterdir():
            if d.is_dir() and d.stat().st_mtime < cutoff:
                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"備份數量: {len(backup_workflows)} / {len(all_workflows)} workflows")
    print(f"SHA256: {compute_sha256(backup_workflows)}")


if __name__ == "__main__":
    main()
