#!/usr/bin/env python3
"""Pack a Hermes or OpenClaw instance into a Grantfold vault file.

Writes memory, identity, and skill names. Refuses credentials, env, auth,
raw sessions, databases, and media.

Usage:
  grantfold-pack.py hermes [--home ~/.hermes] [--profile NAME] -o pack.json
  grantfold-pack.py openclaw [--home ~/.openclaw] -o pack.json
"""
from __future__ import annotations

import argparse
import hashlib
import json
import re
import sys
from datetime import datetime, timezone
from pathlib import Path

SCHEMA = "grantfold.pack.v1"
MAX_NOTE = 12000
MAX_NOTES = 80
SECRET_NAME = re.compile(
    r"(^|/)(\.env.*|auth\.json|.*\.pem|.*\.key|credentials|secret|token|"
    r".*\.sqlite.*|.*\.db|local\.db|.*\.provision|.*cookie.*)$",
    re.I,
)
IDENTITY_NAMES = {
    "USER.md",
    "MEMORY.md",
    "SOUL.md",
    "IDENTITY.md",
    "AGENTS.md",
    "HEARTBEAT.md",
    "TOOLS.md",
}


def refuse(path: Path, reason: str, refused: list) -> None:
    refused.append({"path": str(path), "reason": reason})


def is_secret(path: Path) -> bool:
    text = str(path).replace("\\", "/")
    name = path.name
    if SECRET_NAME.search(text) or SECRET_NAME.search(name):
        return True
    if name.endswith((".png", ".jpg", ".jpeg", ".gif", ".webp", ".mp3", ".mp4", ".mov", ".zip")):
        return True
    return False


def read_text(path: Path) -> str | None:
    try:
        raw = path.read_text(encoding="utf-8", errors="replace")
    except OSError:
        return None
    return raw.strip()


def skill_blurb(path: Path) -> tuple[str, str] | None:
    text = read_text(path)
    if not text or not text.startswith("---"):
        return None
    parts = text.split("---", 2)
    if len(parts) < 3:
        return None
    name = ""
    desc = ""
    for line in parts[1].splitlines():
        if line.startswith("name:"):
            name = line.split(":", 1)[1].strip().strip("\"'")
        elif line.startswith("description:"):
            desc = line.split(":", 1)[1].strip().strip("\"'")
    if not name:
        name = path.parent.name
    return name, desc[:240]


def add_note(notes: list, title: str, body: str, layer: str, source: str) -> None:
    body = (body or "").strip()
    if not body or len(notes) >= MAX_NOTES:
        return
    notes.append(
        {
            "id": hashlib.sha256(f"{source}:{title}".encode()).hexdigest()[:16],
            "title": title[:120],
            "body": body[:MAX_NOTE],
            "layer": layer,
            "source": source,
        }
    )


def walk_identity(root: Path, notes: list, refused: list, layer: str) -> None:
    if not root.exists():
        return
    for path in sorted(root.rglob("*.md")):
        if is_secret(path):
            refuse(path, "secret-or-binary", refused)
            continue
        if path.name not in IDENTITY_NAMES:
            continue
        body = read_text(path)
        if not body:
            continue
        add_note(notes, path.stem.replace("_", " "), body, layer, str(path.name))


def walk_skills(root: Path, skills: list, refused: list) -> None:
    if not root.exists():
        return
    for path in sorted(root.rglob("SKILL.md")):
        if is_secret(path):
            refuse(path, "secret-or-binary", refused)
            continue
        blurb = skill_blurb(path)
        if not blurb:
            refuse(path, "no-frontmatter", refused)
            continue
        name, desc = blurb
        skills.append({"name": name, "description": desc})


def pack_hermes(home: Path, profile: str) -> dict:
    root = home / "profiles" / profile if profile and profile != "default" else home
    notes: list = []
    skills: list = []
    refused: list = []
    walk_identity(root / "memories", notes, refused, "identity")
    for name in ("SOUL.md", "USER.md", "MEMORY.md"):
        path = root / name
        if path.exists() and not is_secret(path):
            body = read_text(path)
            if body:
                add_note(notes, path.stem, body, "identity", path.name)
    walk_skills(root / "skills", skills, refused)
    for banned in (root / "auth.json", root / ".env", root / "sessions"):
        if banned.exists():
            refuse(banned, "credentials-or-raw-sessions", refused)
    return finish("hermes", str(root), notes, skills, refused)


def pack_openclaw(home: Path) -> dict:
    notes: list = []
    skills: list = []
    refused: list = []
    walk_identity(home / "workspace", notes, refused, "identity")
    for agent in sorted((home / "agents").glob("*")) if (home / "agents").exists() else []:
        walk_identity(agent, notes, refused, "identity")
        walk_skills(agent / "skills", skills, refused)
    walk_skills(home / "skills", skills, refused)
    for banned in (home / "credentials", home / "openclaw.json", home / "memory", home / "browser"):
        if banned.exists():
            refuse(banned, "credentials-or-raw-store", refused)
    return finish("openclaw", str(home), notes, skills, refused)


def finish(source: str, instance: str, notes: list, skills: list, refused: list) -> dict:
    seen = set()
    unique_skills = []
    for row in skills:
        key = row["name"]
        if key in seen:
            continue
        seen.add(key)
        unique_skills.append(row)
    return {
        "schema": SCHEMA,
        "source": source,
        "instance": instance,
        "created": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
        "notes": notes,
        "skills": unique_skills[:200],
        "refused": refused[:200],
    }


def main() -> int:
    parser = argparse.ArgumentParser(description="Pack Hermes or OpenClaw into Grantfold.")
    parser.add_argument("kind", choices=["hermes", "openclaw"])
    parser.add_argument("--home", default="")
    parser.add_argument("--profile", default="default")
    parser.add_argument("-o", "--output", required=True)
    args = parser.parse_args()
    if args.kind == "hermes":
        home = Path(args.home or Path.home() / ".hermes").expanduser()
        pack = pack_hermes(home, args.profile)
    else:
        home = Path(args.home or Path.home() / ".openclaw").expanduser()
        pack = pack_openclaw(home)
    out = Path(args.output).expanduser()
    out.write_text(json.dumps(pack, indent=2))
    print(f"notes {len(pack['notes'])} skills {len(pack['skills'])} refused {len(pack['refused'])} -> {out}")
    return 0


if __name__ == "__main__":
    sys.exit(main())
