#!/usr/bin/env python3
"""
Agent config export tool.

Reads Buzz Desktop's managed-agents.json, exports a redacted allowlist
version of each agent into a git-tracked repo at ~/projects/agent-configs.

Usage:
    python3 export.py [--event-id <nostr-event-id>] [--requested-by <agent-name>]

Environment:
    BUZZ_AGENTS_SOURCE  Override source file path (default: managed-agents.json)
"""

import hashlib
import json
import os
import re
import shutil
import subprocess
import sys
import time

SOURCE_PATH = os.environ.get(
    "BUZZ_AGENTS_SOURCE",
    os.path.expanduser(
        "~/Library/Application Support/xyz.block.buzz.app/agents/managed-agents.json"
    ),
)

REPO_DIR = os.path.expanduser("~/projects/agent-configs")

# Allowlist: ONLY these fields are exported. Everything else is dropped.
ALLOWLIST = [
    "name",
    "display_name",
    "system_prompt",
    "model",
    "runtime",
    "respond_to",
    "parallelism",
    "turn_timeout_seconds",
    "start_on_app_launch",
    "is_builtin",
]

# Known secret patterns for the pre-commit scan
SECRET_PATTERNS = [
    re.compile(r"[A-Za-z0-9+/]{40,}={0,2}"),  # base64-like tokens (40+ chars)
    re.compile(r"sk-[A-Za-z0-9]{20,}"),  # OpenAI-style keys
    re.compile(r"npub1[a-z0-9]{56}"),  # Nostr npub
    re.compile(r"[0-9a-f]{64}"),  # 64-hex (pubkey, sha256)
    re.compile(r"auth_tag\s*[:=]\s*['\"]?[0-9a-f]{40,}"),  # auth_tag values
    re.compile(r"BUZZ_[A-Z_]+\s*[:=]\s*['\"][A-Za-z0-9+/]{20,}"),  # env var secrets
]


def read_source(max_retries=3):
    """Read and parse the source JSON file with retry on failure."""
    for attempt in range(1, max_retries + 1):
        try:
            with open(SOURCE_PATH, "r") as f:
                return json.load(f)
        except (json.JSONDecodeError, FileNotFoundError, OSError) as e:
            if attempt < max_retries:
                time.sleep(0.25)
                continue
            print(f"ERROR: Failed to read {SOURCE_PATH} after {max_retries} attempts: {e}", file=sys.stderr)
            sys.exit(1)


def dedupe_agents(entries):
    """Keep only the last non-builtin entry per agent name."""
    agents = {}
    for entry in entries:
        if entry.get("is_builtin"):
            continue
        name = entry.get("name")
        if name:
            agents[name] = entry
    return agents


def extract_agent(agent_entry):
    """Extract only the allowlist fields from an agent entry."""
    return {k: agent_entry.get(k) for k in ALLOWLIST}


def anonymize_prompt(prompt):
    """Replace 'James' / 'James Lopez' with 'primaryUser' in system_prompt text."""
    if not isinstance(prompt, str):
        return prompt
    return prompt.replace("James Lopez", "primaryUser").replace("James", "primaryUser")


def content_hash(data):
    """Compute a SHA-256 hash of the serialized agent data (matches format_agent_json output)."""
    return hashlib.sha256(
        (json.dumps(data, indent=2, sort_keys=False) + "\n").encode("utf-8")
    ).hexdigest()


def format_agent_md(name, extracted):
    """Format an agent's extracted data as a markdown file."""
    lines = [f"# {name}", ""]
    for key in ALLOWLIST:
        value = extracted.get(key)
        if key == "system_prompt":
            lines.append(f"## system_prompt")
            lines.append("")
            lines.append(value if isinstance(value, str) else str(value))
            lines.append("")
        elif value is not None:
            lines.append(f"### {key}")
            lines.append("")
            lines.append(f"`{value}`")
            lines.append("")
    return "\n".join(lines)


def format_agent_json(name, extracted):
    """Format an agent's extracted data as a JSON file."""
    return json.dumps(extracted, indent=2, sort_keys=False) + "\n"


def scan_for_secrets(text):
    """Check text against known secret patterns. Returns list of violations."""
    violations = []
    for pattern in SECRET_PATTERNS:
        for match in pattern.finditer(text):
            # Skip false positives: short strings, common non-secrets
            matched = match.group()
            if len(matched) < 32:
                continue
            # Skip UUIDs (8-4-4-4-12 format)
            if re.match(r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}", matched):
                continue
            violations.append(matched[:20] + "..." if len(matched) > 20 else matched)
    return violations


def pre_commit_scan(agent_text):
    """Run a pre-commit secret scan on the exported text."""
    violations = scan_for_secrets(agent_text)
    if violations:
        print(f"WARNING: Secret scan found {len(violations)} potential secrets:", file=sys.stderr)
        for v in violations:
            print(f"  - {v}", file=sys.stderr)
        print("Review the export before committing. Use --force to override.", file=sys.stderr)
        return False
    return True


def get_git_log_hash(repo_dir, filename):
    """Get the content hash of the last committed version of a file, if any."""
    try:
        result = subprocess.run(
            ["git", "-C", repo_dir, "show", f"HEAD:{filename}"],
            capture_output=True, text=True, timeout=10
        )
        if result.returncode == 0:
            return hashlib.sha256(result.stdout.encode("utf-8")).hexdigest()
    except (subprocess.TimeoutExpired, FileNotFoundError):
        pass
    return None


def slugify(name):
    """Convert agent name to a filesystem-safe slug."""
    slug = name.lower().strip()
    slug = re.sub(r"[^a-z0-9]+", "-", slug)
    slug = slug.strip("-")
    return slug


def main():
    # Parse arguments
    event_id = None
    requested_by = None
    force = False
    args = sys.argv[1:]
    i = 0
    while i < len(args):
        if args[i] == "--event-id" and i + 1 < len(args):
            event_id = args[i + 1]
            i += 2
        elif args[i] == "--requested-by" and i + 1 < len(args):
            requested_by = args[i + 1]
            i += 2
        elif args[i] == "--force":
            force = True
            i += 1
        else:
            print(f"Unknown argument: {args[i]}", file=sys.stderr)
            sys.exit(1)

    # Read and deduplicate
    entries = read_source()
    agents = dedupe_agents(entries)

    if not agents:
        print("No non-builtin agents found. Nothing to export.", file=sys.stderr)
        sys.exit(0)

    # Ensure repo exists
    os.makedirs(REPO_DIR, exist_ok=True)
    subprocess.run(["git", "-C", REPO_DIR, "init"], capture_output=True)
    # Configure git for this repo (local tool needs config)
    subprocess.run(
        ["git", "-C", REPO_DIR, "config", "user.name", "Agent Config Export"],
        capture_output=True,
    )
    subprocess.run(
        ["git", "-C", REPO_DIR, "config", "user.email", "agent-config@local"],
        capture_output=True,
    )

    # Export each agent
    exported = 0
    for agent_name, agent_entry in sorted(agents.items()):
        extracted = extract_agent(agent_entry)
        # Anonymize system_prompt
        if "system_prompt" in extracted:
            extracted["system_prompt"] = anonymize_prompt(extracted["system_prompt"])

        slug = slugify(agent_name)
        filename = f"agents/{slug}.json"

        content = format_agent_json(agent_name, extracted)
        file_hash = content_hash(extracted)

        # Check if content changed (idempotency)
        existing_hash = get_git_log_hash(REPO_DIR, filename)
        if existing_hash == file_hash:
            print(f"  {agent_name}: unchanged, skipping")
            continue

        # Check committed version BEFORE writing (idempotency)
        committed_hash = get_git_log_hash(REPO_DIR, filename)
        if committed_hash == file_hash:
            print(f"  {agent_name}: unchanged, skipping")
            continue

        # Pre-commit secret scan (before writing anything)
        if not pre_commit_scan(content):
            if not force:
                print(f"  {agent_name}: scan failed, not committing. Use --force to override.", file=sys.stderr)
                continue
            else:
                print(f"  {agent_name}: scan warnings, forcing commit.", file=sys.stderr)

        # Now write to staging area
        os.makedirs(os.path.join(REPO_DIR, "agents"), exist_ok=True)
        filepath = os.path.join(REPO_DIR, filename)
        with open(filepath, "w") as f:
            f.write(content)

        subprocess.run(["git", "-C", REPO_DIR, "add", filename], capture_output=True)
        exported += 1
        print(f"  {agent_name}: exported -> {filename}")

    if exported == 0:
        print("No changes to commit. Repo is up to date.")
        return

    # Build commit message
    agent_list = ", ".join(sorted(agents.keys()))
    msg = f"chore: export {exported} agent config(s) ({agent_list})"

    if event_id:
        msg += f"\n\nConfig-Change-Origin: {event_id}"
    if requested_by:
        msg += f"\nRequested-By: {requested_by}"

    # Commit
    result = subprocess.run(
        ["git", "-C", REPO_DIR, "commit", "-m", msg],
        capture_output=True, text=True
    )
    if result.returncode != 0:
        print(f"Commit failed: {result.stderr}", file=sys.stderr)
        sys.exit(1)

    print(f"\nExported {exported} agent(s) to {REPO_DIR}")
    print(f"  git log:")
    for line in result.stdout.strip().split("\n"):
        print(f"    {line}")


if __name__ == "__main__":
    main()
