"""Generate AI agent context docs (AGENTS.md + provider overlays) from the roam index.

Produces a concise, structured markdown document designed to give AI coding
agents instant codebase comprehension.  The output covers architecture, key
files, entry points, hotspots, test patterns, and health -- all derived from
the pre-built roam index so the document is always current.

Usage:
    roam agent-export                               # print default CLAUDE.md-style output
    roam agent-export --write                       # write CLAUDE.md to project root
    roam agent-export --format agents               # AGENTS.md variant
    roam agent-export --format codex --write        # CODEX.md
    roam agent-export --profile gemini --bundle --write  # AGENTS.md + GEMINI.md
    roam agent-export -o path/OUT.md                # write to specific file

Output formats: text (default), ``--json``. SARIF is deliberately NOT
emitted because agent-export outputs are agent-context Markdown synthesis
— not per-location violations. SARIF is reserved for findings with
file:line coordinates; agent-export's primary deliverable is the
agent-context Markdown document. See action.yml _SUPPORTED_SARIF allowlist
+ W1175-RESEARCH Bucket C propagation plan + W1148 audit memo.
"""

from __future__ import annotations

import os
from collections import Counter

import click

from roam.capability import roam_capability
from roam.commands.resolve import ensure_index
from roam.db.connection import find_project_root, open_db
from roam.output.formatter import json_envelope, to_json

# ---------------------------------------------------------------------------
# Marker for auto-generated sections
# ---------------------------------------------------------------------------

_MARKER_START = "<!-- Auto-generated by roam-code. Regenerate with: roam agent-export --write -->"
_MARKER_END = "<!-- End roam-code auto-generated section -->"


# ---------------------------------------------------------------------------
# Format-specific templates
# ---------------------------------------------------------------------------

_HEADERS = {
    "claude": "# {project_name}\n" + _MARKER_START,
    "agents": "# {project_name} -- Agent Guide\n" + _MARKER_START,
    "cursor": "# {project_name} -- Cursor Rules\n" + _MARKER_START,
    "codex": "# {project_name} -- Codex Instructions\n" + _MARKER_START,
    "gemini": "# {project_name} -- Gemini Instructions\n" + _MARKER_START,
}

_OUTPUT_FILENAMES = {
    "claude": "CLAUDE.md",
    "agents": "AGENTS.md",
    "cursor": ".cursorrules",
    "codex": "CODEX.md",
    "gemini": "GEMINI.md",
}

_PROFILE_PRIMARY_FORMAT = {
    "generic": "agents",
    "claude": "claude",
    "codex": "codex",
    "gemini": "gemini",
    "cursor": "cursor",
    "copilot": "agents",
}

_PROFILE_BUNDLE_FORMATS = {
    "generic": ["agents"],
    "claude": ["agents", "claude"],
    "codex": ["agents", "codex"],
    "gemini": ["agents", "gemini"],
    "cursor": ["agents", "cursor"],
    "copilot": ["agents"],
}


def _resolve_target_formats(
    fmt: str | None,
    profile: str | None,
    bundle: bool,
) -> list[str]:
    """Resolve target output formats from format/profile/bundle flags."""
    if fmt:
        targets = ["agents", fmt] if bundle and fmt != "agents" else [fmt]
    else:
        resolved_profile = profile or "claude"
        if bundle:
            targets = list(_PROFILE_BUNDLE_FORMATS[resolved_profile])
        else:
            targets = [_PROFILE_PRIMARY_FORMAT[resolved_profile]]

    # Deduplicate while preserving order.
    deduped: list[str] = []
    seen: set[str] = set()
    for target in targets:
        if target not in seen:
            deduped.append(target)
            seen.add(target)
    return deduped


# ---------------------------------------------------------------------------
# Data gathering (single DB connection, minimal graph computation)
# ---------------------------------------------------------------------------


def _gather_languages(conn):
    """Language distribution sorted by file count descending."""
    rows = conn.execute(
        "SELECT language, COUNT(*) as cnt FROM files WHERE language IS NOT NULL GROUP BY language ORDER BY cnt DESC"
    ).fetchall()
    total = sum(r["cnt"] for r in rows)
    results = []
    for r in rows:
        pct = round(r["cnt"] * 100 / total, 1) if total else 0
        results.append({"name": r["language"], "files": r["cnt"], "pct": pct})
    return results, total


def _gather_stats(conn):
    """Basic codebase statistics."""
    file_count = conn.execute("SELECT COUNT(*) FROM files").fetchone()[0]
    sym_count = conn.execute("SELECT COUNT(*) FROM symbols").fetchone()[0]
    edge_count = conn.execute("SELECT COUNT(*) FROM edges").fetchone()[0]

    # LOC estimate from symbol_metrics
    loc_estimate = 0
    try:
        row = conn.execute("SELECT SUM(line_count) FROM symbol_metrics").fetchone()
        if row and row[0]:
            loc_estimate = row[0]
    except Exception as _exc:  # noqa: BLE001 — defensive
        from roam.observability import log_swallowed

        log_swallowed("cmd_agent_export:loc_estimate", _exc)

    return {
        "files": file_count,
        "symbols": sym_count,
        "edges": edge_count,
        "loc": loc_estimate,
    }


def _gather_directory_layout(conn):
    """Top-level directory structure with file counts and roles."""
    rows = conn.execute("SELECT path, file_role FROM files").fetchall()

    dirs = {}
    for r in rows:
        p = r["path"].replace("\\", "/")
        parts = p.split("/")
        top_dir = parts[0] if len(parts) > 1 else "."
        if top_dir not in dirs:
            dirs[top_dir] = {"count": 0, "roles": {}}
        dirs[top_dir]["count"] += 1
        role = r["file_role"] or "source"
        dirs[top_dir]["roles"][role] = dirs[top_dir]["roles"].get(role, 0) + 1

    # Sort by file count descending, limit to top 15
    sorted_dirs = sorted(dirs.items(), key=lambda x: -x[1]["count"])[:15]
    results = []
    for name, info in sorted_dirs:
        dominant_role = max(info["roles"], key=info["roles"].get) if info["roles"] else "source"
        results.append(
            {
                "name": name,
                "files": info["count"],
                "role": dominant_role,
            }
        )
    return results


def _gather_key_files(conn, limit=15):
    """Top files by PageRank with one-line descriptions."""
    rows = conn.execute(
        "SELECT f.path, "
        "COALESCE(MAX(gm.pagerank), 0) as max_pr, "
        "COALESCE(MAX(gm.in_degree), 0) as max_in, "
        "COUNT(DISTINCT s.id) as sym_count "
        "FROM files f "
        "JOIN symbols s ON s.file_id = f.id "
        "LEFT JOIN graph_metrics gm ON gm.symbol_id = s.id "
        "GROUP BY f.id "
        "ORDER BY max_pr DESC "
        "LIMIT ?",
        (limit,),
    ).fetchall()

    results = []
    for r in rows:
        results.append(
            {
                "path": r["path"],
                # W-dogfood (W336 sibling): 6-decimal precision —
                # `max_pr` is the MAX symbol-level PR per file. On
                # 5K+ symbol graphs the per-node PR floor is ~1.4e-05,
                # so 4-decimal rounding zeroes ~72% of nonzero values.
                "pagerank": round(r["max_pr"] or 0, 6),
                "fan_in": r["max_in"] or 0,
                "symbols": r["sym_count"],
            }
        )
    return results


def _gather_entry_points(conn, limit=10):
    """Files with no importers that have symbols (likely entry points)."""
    try:
        rows = conn.execute(
            "SELECT f.path, f.language, COUNT(s.id) as sym_count "
            "FROM files f "
            "JOIN symbols s ON s.file_id = f.id "
            "WHERE f.id NOT IN (SELECT DISTINCT target_file_id FROM file_edges) "
            "GROUP BY f.id "
            "HAVING sym_count > 0 "
            "ORDER BY sym_count DESC "
            "LIMIT ?",
            (limit,),
        ).fetchall()
        return [{"path": r["path"], "symbols": r["sym_count"]} for r in rows]
    except Exception as _exc:  # noqa: BLE001 — defensive
        from roam.observability import log_swallowed

        log_swallowed("cmd_agent_export:entry_points", _exc)
        return []


def _gather_hotspots(conn, limit=10):
    """Files with highest churn * complexity."""
    try:
        rows = conn.execute(
            "SELECT f.path, fs.total_churn, fs.commit_count, "
            "fs.distinct_authors, fs.complexity "
            "FROM file_stats fs "
            "JOIN files f ON fs.file_id = f.id "
            "WHERE fs.total_churn > 0 "
            "AND f.file_role = 'source' "
            "ORDER BY COALESCE(fs.complexity, 0) * fs.total_churn DESC "
            "LIMIT ?",
            (limit,),
        ).fetchall()
        results = []
        for r in rows:
            results.append(
                {
                    "path": r["path"],
                    "churn": r["total_churn"] or 0,
                    "commits": r["commit_count"] or 0,
                    "authors": r["distinct_authors"] or 0,
                    "complexity": round(r["complexity"] or 0, 1),
                }
            )
        return results
    except Exception as _exc:  # noqa: BLE001 — defensive
        from roam.observability import log_swallowed

        log_swallowed("cmd_agent_export:hotspots", _exc)
        return []


def _gather_test_info(conn):
    """Test framework detection and coverage stats."""
    # Count test files vs source files
    try:
        total_files = conn.execute("SELECT COUNT(*) FROM files").fetchone()[0]
        test_files = conn.execute("SELECT COUNT(*) FROM files WHERE file_role = 'test'").fetchone()[0]
        source_files = conn.execute("SELECT COUNT(*) FROM files WHERE file_role = 'source'").fetchone()[0]
    except Exception as _exc:  # noqa: BLE001 — defensive
        from roam.observability import log_swallowed

        log_swallowed("cmd_agent_export:test_info_counts", _exc)
        total_files = 0
        test_files = 0
        source_files = 0

    # Detect test directory structure
    test_dirs = set()
    try:
        rows = conn.execute("SELECT path FROM files WHERE file_role = 'test' LIMIT 50").fetchall()
        for r in rows:
            p = r["path"].replace("\\", "/")
            parts = p.split("/")
            if len(parts) > 1:
                test_dirs.add(parts[0])
    except Exception as _exc:  # noqa: BLE001 — defensive
        from roam.observability import log_swallowed

        log_swallowed("cmd_agent_export:test_dirs", _exc)

    # Detect test naming convention
    test_patterns = set()
    try:
        rows = conn.execute("SELECT path FROM files WHERE file_role = 'test' LIMIT 50").fetchall()
        for r in rows:
            basename = os.path.basename(r["path"])
            if basename.startswith("test_"):
                test_patterns.add("test_*.py")
            elif basename.endswith("_test.py"):
                test_patterns.add("*_test.py")
            elif basename.endswith("_test.go"):
                test_patterns.add("*_test.go")
            elif ".test." in basename:
                ext = basename.rsplit(".", 1)[-1] if "." in basename else ""
                test_patterns.add(f"*.test.{ext}")
            elif ".spec." in basename:
                ext = basename.rsplit(".", 1)[-1] if "." in basename else ""
                test_patterns.add(f"*.spec.{ext}")
            elif basename.endswith("Test.java"):
                test_patterns.add("*Test.java")
            elif basename.endswith("_spec.rb"):
                test_patterns.add("*_spec.rb")
    except Exception as _exc:  # noqa: BLE001 — defensive
        from roam.observability import log_swallowed

        log_swallowed("cmd_agent_export:test_patterns", _exc)

    return {
        "total_files": total_files,
        "test_files": test_files,
        "source_files": source_files,
        "test_dirs": sorted(test_dirs),
        "test_patterns": sorted(test_patterns),
    }


def _gather_health(conn):
    """Lightweight health summary without full graph computation.

    W17.2 / Pattern 3c: overrides ``cycles`` and ``god_components`` with
    the canonical numbers from ``roam.quality.cycles`` and
    ``roam.quality.god_components`` so agent-export, health, and
    describe always agree. The raw ``metrics_history`` numbers are kept
    as ``cycles_raw`` / ``god_components_raw`` for backward compatibility.
    """
    from roam.commands.metrics_history import collect_metrics
    from roam.quality.cycles import cycles_summary
    from roam.quality.god_components import god_components

    try:
        m = collect_metrics(conn)
    except Exception as _exc:  # noqa: BLE001 — defensive
        from roam.observability import log_swallowed

        log_swallowed("cmd_agent_export:health_metrics", _exc)
        m = {
            "health_score": None,
            "cycles": 0,
            "god_components": 0,
            "dead_exports": 0,
            "layer_violations": 0,
            "tangle_ratio": 0,
        }
    try:
        csum = cycles_summary(conn)
        m["cycles_raw"] = m.get("cycles", 0)
        m["cycles"] = csum.actionable
        m["cycles_total"] = csum.total
        m["cycles_actionable"] = csum.actionable
        m["cycles_informational"] = csum.informational
    except Exception as _exc:  # noqa: BLE001 — defensive
        from roam.observability import log_swallowed

        log_swallowed("cmd_agent_export:cycles_summary", _exc)
    try:
        gsum = god_components(conn)
        m["god_components_raw"] = m.get("god_components", 0)
        m["god_components"] = gsum.total
        m["god_components_critical"] = gsum.critical
        m["god_components_actionable"] = gsum.actionable
    except Exception as _exc:  # noqa: BLE001 — defensive
        from roam.observability import log_swallowed

        log_swallowed("cmd_agent_export:god_components", _exc)
    return m


def _gather_layers(conn):
    """Architecture layer summary."""
    try:
        from roam.graph.builder import build_symbol_graph
        from roam.graph.layers import detect_layers

        G = build_symbol_graph(conn)
        layer_map = detect_layers(G)
        if not layer_map:
            return []

        max_layer = max(layer_map.values())
        layers_list = [set() for _ in range(max_layer + 1)]
        for node_id, layer_num in layer_map.items():
            layers_list[layer_num].add(node_id)

        results = []
        for layer_num, sym_ids in enumerate(layers_list):
            if not sym_ids:
                continue
            # Get top directories for this layer
            sym_list = list(sym_ids)[:200]
            ph = ",".join("?" for _ in sym_list)
            dir_rows = conn.execute(
                f"SELECT CASE WHEN INSTR(REPLACE(f.path, '\\', '/'), '/') > 0 "
                f"THEN SUBSTR(REPLACE(f.path, '\\', '/'), 1, "
                f"INSTR(REPLACE(f.path, '\\', '/'), '/') - 1) "
                f"ELSE '.' END as dir "
                f"FROM symbols s JOIN files f ON s.file_id = f.id "
                f"WHERE s.id IN ({ph})",
                sym_list,
            ).fetchall()
            dirs = Counter(r["dir"] for r in dir_rows)
            top_dirs = sorted(dirs.items(), key=lambda x: -x[1])[:3]
            results.append(
                {
                    "layer": layer_num,
                    "size": len(sym_ids),
                    "dirs": [d for d, _ in top_dirs],
                }
            )
        return results
    except Exception as _exc:  # noqa: BLE001 — defensive
        from roam.observability import log_swallowed

        log_swallowed("cmd_agent_export:layers", _exc)
        return []


def _gather_clusters(conn, limit=8):
    """Module cluster summary."""
    try:
        rows = conn.execute(
            "SELECT cluster_id, cluster_label, COUNT(*) as size "
            "FROM clusters GROUP BY cluster_id ORDER BY size DESC "
            "LIMIT ?",
            (limit,),
        ).fetchall()
        return [
            {
                "label": r["cluster_label"] or f"cluster-{r['cluster_id']}",
                "size": r["size"],
            }
            for r in rows
        ]
    except Exception as _exc:  # noqa: BLE001 — defensive
        from roam.observability import log_swallowed

        log_swallowed("cmd_agent_export:clusters", _exc)
        return []


def _detect_build_command(conn):
    """Detect likely build/test commands from project files."""
    file_names = set()
    try:
        rows = conn.execute("SELECT path FROM files LIMIT 500").fetchall()
        for r in rows:
            name = os.path.basename(r["path"]).lower()
            file_names.add(name)
    except Exception as _exc:  # noqa: BLE001 — defensive
        from roam.observability import log_swallowed

        log_swallowed("cmd_agent_export:build_command", _exc)
        return None, None

    build_cmd = None
    test_cmd = None

    if "pyproject.toml" in file_names or "setup.py" in file_names:
        build_cmd = "pip install -e ."
        test_cmd = "pytest tests/"
    elif "package.json" in file_names:
        build_cmd = "npm install"
        test_cmd = "npm test"
    elif "cargo.toml" in file_names:
        build_cmd = "cargo build"
        test_cmd = "cargo test"
    elif "go.mod" in file_names:
        build_cmd = "go build ./..."
        test_cmd = "go test ./..."
    elif "pom.xml" in file_names:
        build_cmd = "mvn compile"
        test_cmd = "mvn test"
    elif "build.gradle" in file_names or "build.gradle.kts" in file_names:
        build_cmd = "gradle build"
        test_cmd = "gradle test"
    elif "makefile" in file_names:
        build_cmd = "make"
        test_cmd = "make test"
    elif "gemfile" in file_names:
        build_cmd = "bundle install"
        test_cmd = "bundle exec rspec"
    elif "composer.json" in file_names:
        build_cmd = "composer install"
        test_cmd = "vendor/bin/phpunit"

    return build_cmd, test_cmd


def _detect_frameworks(conn):
    """Lightweight framework detection reusing cmd_understand logic."""
    try:
        from roam.commands.cmd_understand import _detect_build, _detect_frameworks

        frameworks = _detect_frameworks(conn)
        build_tool = _detect_build(conn)
        return frameworks, build_tool
    except Exception as _exc:  # noqa: BLE001 — defensive
        from roam.observability import log_swallowed

        log_swallowed("cmd_agent_export:frameworks", _exc)
        return [], None


# ---------------------------------------------------------------------------
# Markdown generation
# ---------------------------------------------------------------------------


def _generate_markdown(
    project_name,
    fmt,
    languages,
    stats,
    layout,
    key_files,
    entry_points,
    hotspots,
    test_info,
    health,
    layers,
    clusters,
    build_cmd,
    test_cmd,
    frameworks,
    build_tool,
):
    """Generate the complete markdown document."""
    lines = []

    # Header
    header = _HEADERS.get(fmt, _HEADERS["claude"]).format(project_name=project_name)
    lines.append(header)
    lines.append("")

    # -- Project Overview --
    lines.append("## What this project is")
    lines.append("")
    lang_str = ", ".join(f"{l['name']} ({l['pct']:.0f}%)" for l in languages[:5])
    if len(languages) > 5:
        lang_str += f", +{len(languages) - 5} more"
    lines.append(f"**Languages:** {lang_str}")
    lines.append(
        f"**Size:** {stats['files']} files, {stats['symbols']} symbols"
        + (f", ~{stats['loc']:,} LOC" if stats["loc"] else "")
    )
    if frameworks:
        lines.append(f"**Stack:** {', '.join(frameworks)}" + (f" | Build: {build_tool}" if build_tool else ""))
    lines.append("")

    # -- Architecture --
    lines.append("## Architecture")
    lines.append("")

    # Directory layout
    if layout:
        lines.append("### Directory layout")
        lines.append("")
        lines.append("```")
        for d in layout:
            role_hint = f" ({d['role']}, {d['files']} files)" if d["name"] != "." else f" (root, {d['files']} files)"
            lines.append(f"  {d['name'] + '/':<30s}{role_hint}")
        lines.append("```")
        lines.append("")

    # Architecture layers
    if layers:
        lines.append("### Layers")
        lines.append("")
        for layer in layers:
            label = "foundation" if layer["layer"] == 0 else f"L{layer['layer']}"
            dirs_str = ", ".join(f"{d}/" for d in layer["dirs"][:3])
            lines.append(f"- **{label}** ({layer['size']} symbols): {dirs_str}")
        lines.append("")

    # Module clusters
    if clusters:
        lines.append("### Modules")
        lines.append("")
        for cl in clusters:
            lines.append(f"- {cl['label']} ({cl['size']} symbols)")
        lines.append("")

    # -- Key Files --
    if key_files:
        lines.append("## Key Files (by importance)")
        lines.append("")
        for kf in key_files:
            lines.append(f"- `{kf['path']}` -- {kf['symbols']} symbols, PageRank {kf['pagerank']}")
        lines.append("")

    # -- Entry Points --
    if entry_points:
        lines.append("## Entry Points")
        lines.append("")
        for ep in entry_points:
            lines.append(f"- `{ep['path']}` ({ep['symbols']} symbols)")
        lines.append("")

    # -- Hotspots --
    if hotspots:
        lines.append("## Hot Spots (change with care)")
        lines.append("")
        for hs in hotspots:
            parts = [f"{hs['churn']} changes"]
            if hs["complexity"]:
                parts.append(f"complexity {hs['complexity']}")
            if hs["authors"]:
                parts.append(f"{hs['authors']} authors")
            lines.append(f"- `{hs['path']}` -- {', '.join(parts)}")
        lines.append("")

    # -- Test Patterns --
    lines.append("## Test Patterns")
    lines.append("")
    if test_info["test_dirs"]:
        lines.append(f"**Test directories:** {', '.join(d + '/' for d in test_info['test_dirs'])}")
    if test_info["test_patterns"]:
        lines.append(f"**Naming convention:** {', '.join(test_info['test_patterns'])}")
    lines.append(f"**Coverage:** {test_info['test_files']} test files, {test_info['source_files']} source files")
    lines.append("")

    # -- Common Tasks --
    lines.append("## Common Tasks")
    lines.append("")
    lines.append("```bash")
    if build_cmd:
        lines.append("# Build / install")
        lines.append(build_cmd)
        lines.append("")
    if test_cmd:
        lines.append("# Run tests")
        lines.append(test_cmd)
        lines.append("")
    # Cursor format is more concise -- skip roam commands
    if fmt != "cursor":
        lines.append("# Codebase exploration (if roam-code installed)")
        lines.append("roam search <pattern>       # find symbols")
        lines.append("roam preflight <symbol>     # safety check before changes")
        lines.append("roam context <symbol>       # files + line ranges to read")
        lines.append("roam health                 # codebase health score")
        lines.append("roam agent-export --write   # regenerate this file")
    lines.append("```")
    lines.append("")

    # -- Health Summary --
    if health and health.get("health_score") is not None:
        lines.append("## Health Summary")
        lines.append("")
        score = health["health_score"]
        lines.append(f"**Health score:** {score}/100")
        issues = []
        if health.get("cycles", 0) > 0:
            issues.append(f"{health['cycles']} dependency cycles")
        if health.get("god_components", 0) > 0:
            issues.append(f"{health['god_components']} god components")
        if health.get("dead_exports", 0) > 10:
            issues.append(f"{health['dead_exports']} dead exports")
        if health.get("layer_violations", 0) > 0:
            issues.append(f"{health['layer_violations']} layer violations")
        if health.get("tangle_ratio", 0) > 5:
            issues.append(f"tangle ratio {health['tangle_ratio']}%")
        if issues:
            lines.append(f"**Key issues:** {', '.join(issues)}")
        lines.append("")

    # End marker
    lines.append(_MARKER_END)

    return "\n".join(lines) + "\n"


def _write_with_preserve(filepath, content):
    """Write content to file, preserving manual additions outside markers.

    If the file already contains the auto-generated markers, only the
    section between markers is replaced.  Manual content before/after the
    markers is preserved.

    Writes go through :func:`atomic_write_text` so an interrupted write
    can NEVER truncate the user's manual content sitting outside the
    markers — the temp file is replaced atomically or not at all.
    """
    from roam.atomic_io import atomic_write_text

    if os.path.exists(filepath):
        try:
            with open(filepath, "r", encoding="utf-8") as _fh:
                existing = _fh.read()
        except Exception as _exc:  # noqa: BLE001 — defensive
            from roam.observability import log_swallowed

            log_swallowed("cmd_agent_export:read_existing", _exc)
            existing = ""

        start_idx = existing.find(_MARKER_START)
        end_idx = existing.find(_MARKER_END)

        if start_idx >= 0 and end_idx >= 0:
            # Preserve content before the header line containing the marker
            # Find the start of the line containing the marker
            header_start = existing.rfind("\n", 0, start_idx)
            # Look for the "# Project Name" line above the marker
            if header_start > 0:
                # Check if there's a markdown heading right before the marker
                prev_newline = existing.rfind("\n", 0, header_start)
                if prev_newline >= 0:
                    between = existing[prev_newline + 1 : header_start + 1].strip()
                    if between.startswith("#"):
                        header_start = prev_newline
            if header_start < 0:
                header_start = 0

            end_marker_end = end_idx + len(_MARKER_END)
            # Consume trailing newline after end marker
            if end_marker_end < len(existing) and existing[end_marker_end] == "\n":
                end_marker_end += 1

            before = existing[:header_start].rstrip("\n")
            after = existing[end_marker_end:].lstrip("\n")

            parts = []
            if before:
                parts.append(before)
                parts.append("")
            parts.append(content.rstrip("\n"))
            if after:
                parts.append("")
                parts.append(after)

            final = "\n".join(parts) + "\n"
            atomic_write_text(filepath, final)
            return
    # No existing markers or no existing file -- write fresh
    atomic_write_text(filepath, content)


# ---------------------------------------------------------------------------
# CLI command
# ---------------------------------------------------------------------------


@roam_capability(
    name="agent-export",
    category="getting-started",
    summary="Generate an AI agent context file from the roam index",
    maturity="stable",
    mcp_expose=True,
    mcp_preset=("core",),
    side_effect=True,
    task_required=False,
    destructive=False,
    stale_sensitive=True,
    ai_safe=True,
    requires_index=True,
)
@click.command("agent-export")
@click.option("--output", "-o", default=None, type=click.Path(), help="Output file path (default: stdout)")
@click.option(
    "--format",
    "fmt",
    type=click.Choice(["claude", "agents", "cursor", "codex", "gemini"]),
    default=None,
    help="Output format variant",
)
@click.option(
    "--profile",
    type=click.Choice(["generic", "claude", "codex", "gemini", "cursor", "copilot"]),
    default=None,
    help="Client profile mapping (used with default format selection and bundles)",
)
@click.option("--bundle", is_flag=True, help="Write canonical AGENTS.md plus profile-specific overlay(s)")
@click.option(
    "--write",
    "write_flag",
    is_flag=True,
    help="Write directly to project root (CLAUDE/AGENTS/CODEX/GEMINI/.cursorrules)",
)
@click.option(
    "--brief",
    "brief_mode",
    is_flag=True,
    help="emit a compact summary (top-level fields only) instead of the full agent doc.",
)
@click.pass_context
def agent_export_cmd(ctx, output, fmt, profile, bundle, write_flag, brief_mode):
    """Generate an AI agent context file from the roam index.

    Produces a concise markdown document with architecture, key files,
    entry points, hotspots, test patterns, and health summary -- designed
    to be placed in the project root as CLAUDE.md, AGENTS.md, or
    .cursorrules for instant AI agent comprehension.

    Unlike ``context`` (which returns ad-hoc file + line ranges for a
    specific symbol), this command writes a persistent project-wide
    context document for AI agents.

    \b
    Examples:
        roam agent-export                               # print default CLAUDE.md-style output
        roam agent-export --write                       # write CLAUDE.md
        roam agent-export --format agents               # AGENTS.md variant
        roam agent-export --profile codex --bundle --write  # AGENTS.md + CODEX.md
        roam agent-export -o MY_AGENTS.md               # custom output file
    """
    json_mode = ctx.obj.get("json") if ctx.obj else False
    formats = _resolve_target_formats(fmt, profile, bundle)
    if output and len(formats) > 1:
        from roam.output.errors import INVALID_OPTIONS, structured_usage_error

        raise structured_usage_error(
            INVALID_OPTIONS,
            "--output supports a single format. Remove --bundle or set --format explicitly.",
        )
    if bundle and not write_flag and not json_mode:
        from roam.output.errors import MISSING_REQUIRED_ARG, structured_usage_error

        raise structured_usage_error(
            MISSING_REQUIRED_ARG,
            "--bundle requires --write (or --json for planning output).",
        )

    ensure_index()
    root = find_project_root()
    project_name = root.name

    with open_db(readonly=True) as conn:
        # Gather all data
        languages, lang_total = _gather_languages(conn)
        stats = _gather_stats(conn)
        layout = _gather_directory_layout(conn)
        key_files = _gather_key_files(conn, limit=15)
        entry_points = _gather_entry_points(conn, limit=10)
        hotspots = _gather_hotspots(conn, limit=10)
        test_info = _gather_test_info(conn)
        health = _gather_health(conn)
        layers = _gather_layers(conn)
        clusters = _gather_clusters(conn, limit=8)
        build_cmd, test_cmd = _detect_build_command(conn)
        frameworks, build_tool = _detect_frameworks(conn)

    # Count sections for the verdict
    section_count = sum(
        1
        for x in [
            languages,
            layout,
            key_files,
            entry_points,
            hotspots,
            True,  # test_info always present
            health.get("health_score") is not None if health else False,
            layers,
            clusters,
        ]
        if x
    )

    targets = ", ".join(_OUTPUT_FILENAMES[f] for f in formats)
    if len(formats) == 1:
        verdict = f"Generated {_OUTPUT_FILENAMES[formats[0]]} with {section_count} sections from {stats['files']} files"
    else:
        verdict = f"Generated bundle ({targets}) with {section_count} sections from {stats['files']} files"

    # JSON output
    if json_mode:
        envelope_kwargs = dict(
            format=formats[0],
            formats=formats,
            profile=profile,
            bundle=bundle,
            project_name=project_name,
        )
        if not brief_mode:
            # full payload only when --brief is OFF.
            envelope_kwargs.update(
                languages=languages,
                stats=stats,
                directory_layout=layout,
                key_files=key_files,
                entry_points=entry_points,
                hotspots=hotspots,
                test_info=test_info,
                health_summary=(
                    {
                        "score": health.get("health_score"),
                        "cycles": health.get("cycles", 0),
                        "cycles_total": health.get("cycles_total", health.get("cycles", 0)),
                        "cycles_actionable": health.get("cycles_actionable", health.get("cycles", 0)),
                        "cycles_definition": (__import__("roam.quality.cycles", fromlist=["definition"]).definition()),
                        "god_components": health.get("god_components", 0),
                        "god_components_definition": (
                            __import__("roam.quality.god_components", fromlist=["definition"]).definition()
                        ),
                        "dead_exports": health.get("dead_exports", 0),
                    }
                    if health
                    else None
                ),
                layers=layers,
                clusters=clusters,
            )
        click.echo(
            to_json(
                json_envelope(
                    "agent-export",
                    summary={
                        "verdict": verdict,
                        "format": formats[0],
                        "formats": formats,
                        "profile": profile,
                        "bundle": bundle,
                        "sections": section_count,
                        "files": stats["files"],
                        "symbols": stats["symbols"],
                        "brief": brief_mode,
                    },
                    **envelope_kwargs,
                )
            )
        )
        return

    # Determine where to write
    if write_flag:
        written: list[str] = []
        total_bytes = 0
        for target_fmt in formats:
            content = _generate_markdown(
                project_name,
                target_fmt,
                languages,
                stats,
                layout,
                key_files,
                entry_points,
                hotspots,
                test_info,
                health,
                layers,
                clusters,
                build_cmd,
                test_cmd,
                frameworks,
                build_tool,
            )
            filename = _OUTPUT_FILENAMES[target_fmt]
            filepath = str(root / filename)
            _write_with_preserve(filepath, content)
            written.append(filename)
            total_bytes += len(content.encode("utf-8"))

        size_kb = round(total_bytes / 1024, 1)
        click.echo(f"VERDICT: {verdict}")
        if len(written) == 1:
            click.echo(f"Wrote {written[0]} ({size_kb} KB, {section_count} sections)")
        else:
            click.echo(f"Wrote bundle: {', '.join(written)} ({size_kb} KB total, {section_count} sections each)")
    elif output:
        target_fmt = formats[0]
        content = _generate_markdown(
            project_name,
            target_fmt,
            languages,
            stats,
            layout,
            key_files,
            entry_points,
            hotspots,
            test_info,
            health,
            layers,
            clusters,
            build_cmd,
            test_cmd,
            frameworks,
            build_tool,
        )
        _write_with_preserve(output, content)
        size_kb = round(len(content.encode("utf-8")) / 1024, 1)
        click.echo(f"VERDICT: {verdict}")
        click.echo(f"Wrote {output} ({size_kb} KB, {section_count} sections)")
    else:
        target_fmt = formats[0]
        content = _generate_markdown(
            project_name,
            target_fmt,
            languages,
            stats,
            layout,
            key_files,
            entry_points,
            hotspots,
            test_info,
            health,
            layers,
            clusters,
            build_cmd,
            test_cmd,
            frameworks,
            build_tool,
        )
        click.echo(f"VERDICT: {verdict}\n")
        click.echo(content)
