"""Show project skeleton with entry points and key symbols.

Output formats: text (default), ``--json``. SARIF is deliberately NOT
emitted because map outputs are invocation-scoped project-skeleton
summaries (top PageRank-ranked symbols + entry points grouped by file
kind / language / role) — not per-location code violations. The
skeleton describes the architectural surface of the workspace rather
than defects at source coordinates; SARIF audiences scan for per-
finding rule_id + region rows. See action.yml _SUPPORTED_SARIF
allowlist + W1175-RESEARCH propagation plan + W1224-audit memo.
"""

from __future__ import annotations

from collections import Counter

import click

from roam.capability import roam_capability
from roam.commands.resolve import detect_entry_points, ensure_index
from roam.db.connection import open_db
from roam.db.queries import (
    ALL_FILES,
    TOP_SYMBOLS_BY_PAGERANK,
)
from roam.output.formatter import (
    abbrev_kind,
    format_signature,
    format_table,
    json_envelope,
    loc,
    section,
    to_json,
)


def _estimate_tokens(text: str) -> int:
    """Rough token estimate: 4 characters = 1 token."""
    return max(1, len(text) // 4)


def _build_symbol_entry_text(s, *, for_budget: bool = False) -> str:
    """Build a single symbol's display line for token-budget accounting."""
    sig = format_signature(s["signature"], max_len=50)
    kind = abbrev_kind(s["kind"])
    location = loc(s["file_path"], s["line_start"])
    pr = f"{(s['pagerank'] or 0):.4f}"
    # Approximate the formatted table row as a single string
    return f"{kind}  {s['name']}  {sig}  {location}  {pr}"


@roam_capability(
    name="map",
    category="architecture",
    summary="Show project skeleton with entry points and key symbols",
    maturity="stable",
    mcp_expose=True,
    mcp_preset=("core", "architecture"),
    side_effect=False,
    task_required=False,
    destructive=False,
    stale_sensitive=True,
    ai_safe=True,
    requires_index=True,
)
@click.command("map")
@click.option("-n", "count", default=20, help="Number of top symbols to show")
@click.option("--full", is_flag=True, help="Show all results without truncation")
@click.option("--budget", type=int, default=None, help="Approximate token limit for output")
@click.option(
    "--seed",
    "seed_path",
    type=str,
    default=None,
    help="restrict the top-symbols list to symbols reachable from this file.",
)
@click.option(
    "--depth",
    "seed_depth",
    type=int,
    default=2,
    show_default=True,
    help="BFS hop limit when --seed is given.",
)
@click.pass_context
def map_cmd(ctx, count, full, budget, seed_path, seed_depth):
    """Show project skeleton with entry points and key symbols.

    Unlike ``describe`` (which generates a prose project overview), this
    command produces a structured skeleton with directories, entry points,
    and top symbols by PageRank.
    """
    json_mode = ctx.obj.get("json") if ctx.obj else False
    # The CLI normalises ``--budget`` to the global option (see
    # ``LazyGroup._normalise_global_option_position``) — when a user
    # writes ``roam map --budget 200``, the value lands in
    # ``ctx.obj["budget"]`` rather than the command-local ``budget``
    # parameter. Falling back to the global value keeps both shapes
    # working and makes the test_map_budget_* tests pass without
    # forking the parser. v12.12.
    if budget is None:
        global_budget = ctx.obj.get("budget", 0) if ctx.obj else 0
        if global_budget and global_budget > 0:
            budget = int(global_budget)
    ensure_index()

    with open_db(readonly=True) as conn:
        # --- Project stats ---
        files = conn.execute(ALL_FILES).fetchall()
        total_files = len(files)
        sym_count = conn.execute("SELECT COUNT(*) FROM symbols").fetchone()[0]
        edge_count = conn.execute("SELECT COUNT(*) FROM edges").fetchone()[0]

        lang_counts = Counter(f["language"] for f in files if f["language"])

        # Edge kind distribution
        edge_kinds = conn.execute("SELECT kind, COUNT(*) as cnt FROM edges GROUP BY kind ORDER BY cnt DESC").fetchall()

        # --- Top directories ---
        dir_rows_raw = conn.execute("""
            SELECT CASE WHEN INSTR(REPLACE(path, '\\', '/'), '/') > 0
                   THEN SUBSTR(REPLACE(path, '\\', '/'), 1, INSTR(REPLACE(path, '\\', '/'), '/') - 1)
                   ELSE '.' END as dir,
                   COUNT(*) as cnt
            FROM files GROUP BY dir ORDER BY cnt DESC
        """).fetchall()
        dir_counts = {r["dir"]: r["cnt"] for r in dir_rows_raw}
        dir_items = sorted(dir_counts.items(), key=lambda x: x[1], reverse=True)

        # --- Entry points ---
        entries = [ep["path"] for ep in detect_entry_points(conn)]

        # --- Top symbols by PageRank ---
        # When budget is active, fetch all ranked symbols to allow greedy
        # filling up to the token limit.  Otherwise honour the -n count.
        fetch_limit = count if budget is None else 10_000
        all_ranked = conn.execute(TOP_SYMBOLS_BY_PAGERANK, (fetch_limit,)).fetchall()

        # if a seed file is given, narrow ``all_ranked`` to
        # symbols reachable from that file within ``seed_depth`` hops.
        if seed_path:
            try:
                import networkx as nx

                from roam.graph.builder import build_symbol_graph
            except ImportError:
                seed_path = None  # graph unavailable, fall through
            if seed_path:
                seed_norm = seed_path.replace("\\", "/")
                seed_rows = conn.execute(
                    "SELECT s.id FROM symbols s JOIN files f ON f.id = s.file_id "
                    "WHERE REPLACE(f.path, '\\', '/') = ? OR REPLACE(f.path, '\\', '/') LIKE ?",
                    (seed_norm, f"{seed_norm}%"),
                ).fetchall()
                seed_ids = [r["id"] for r in seed_rows]
                if seed_ids:
                    G = build_symbol_graph(conn)
                    reach: set[int] = set(seed_ids)
                    for sid in seed_ids:
                        if sid not in G:
                            continue
                        try:
                            lengths = nx.single_source_shortest_path_length(G, sid, cutoff=int(seed_depth))
                            reach.update(lengths.keys())
                        except nx.NetworkXException:
                            continue
                    all_ranked = [r for r in all_ranked if r["id"] in reach]

        # ---- Budget-aware symbol selection ----
        if budget is not None:
            # Pre-compute the preamble (stats, dirs, entries) to account
            # for its token cost when filling the budget.
            lang_str = ", ".join(f"{lang}={n}" for lang, n in lang_counts.most_common(8))
            edge_str = ", ".join(f"{r['kind']}={r['cnt']}" for r in edge_kinds) if edge_kinds else "none"

            preamble_lines = [
                f"Files: {total_files}  Symbols: {sym_count}  Edges: {edge_count}",
                f"Languages: {lang_str}",
                f"Edge kinds: {edge_str}",
                "",
            ]
            # Directories section
            dir_rows_budget = [[d, str(c)] for d, c in (dir_items if full else dir_items[:15])]
            preamble_lines.append("Directories:")
            preamble_lines.append(
                format_table(
                    ["dir", "files"],
                    dir_rows_budget,
                    budget=0 if full else 15,
                )
            )
            preamble_lines.append("")

            # Entry points section
            if entries:
                preamble_lines.append("Entry points:")
                for e in entries if full else entries[:20]:
                    preamble_lines.append(f"  {e}")
                if not full and len(entries) > 20:
                    preamble_lines.append(f"  (+{len(entries) - 20} more)")
                preamble_lines.append("")

            preamble_text = "\n".join(preamble_lines)
            tokens_used = _estimate_tokens(preamble_text)

            # Greedily add symbols until budget is exhausted
            top = []
            for s in all_ranked:
                entry_text = _build_symbol_entry_text(s)
                entry_tokens = _estimate_tokens(entry_text)
                if tokens_used + entry_tokens > budget:
                    break
                tokens_used += entry_tokens
                top.append(s)
        else:
            top = all_ranked
            tokens_used = 0  # not tracked when budget is off

        # Build verdict
        n_dirs = len(dir_items)
        deepest = max(dir_items, key=lambda x: x[0].count("/"))[0] if dir_items else "."
        map_verdict = f"map: {n_dirs} directories, {total_files} files, {sym_count} symbols, deepest: {deepest}"

        if json_mode:
            data = {
                "files": total_files,
                "symbols": sym_count,
                "edges": edge_count,
                "languages": dict(lang_counts.most_common(8)),
                "edge_kinds": {r["kind"]: r["cnt"] for r in edge_kinds},
                "directories": [{"name": d, "files": c} for d, c in dir_items],
                "entry_points": entries,
                "top_symbols": [
                    {
                        "name": s["name"],
                        "kind": s["kind"],
                        "signature": s["signature"] or "",
                        "location": loc(s["file_path"], s["line_start"]),
                        # W-dogfood (W336 sibling): 6-decimal precision
                        # — 4-decimal zeroed ~72% of nonzero PR values
                        # on 5K+ symbol graphs (floor ~1.4e-05).
                        "pagerank": round(s["pagerank"] or 0, 6),
                    }
                    for s in top
                ],
            }
            summary = {
                "verdict": map_verdict,
                "files": total_files,
                "symbols": sym_count,
                "edges": edge_count,
            }
            if budget is not None:
                summary["token_budget"] = budget
                summary["tokens_used"] = tokens_used
                data["token_budget"] = budget
                data["tokens_used"] = tokens_used
            click.echo(
                to_json(
                    json_envelope(
                        "map",
                        summary=summary,
                        **data,
                    )
                )
            )
            return

        # --- Text output ---
        click.echo(f"VERDICT: {map_verdict}\n")
        if budget is not None:
            # Preamble already built above; emit it directly
            click.echo(preamble_text, nl=False)
        else:
            lang_str = ", ".join(f"{lang}={n}" for lang, n in lang_counts.most_common(8))
            edge_str = ", ".join(f"{r['kind']}={r['cnt']}" for r in edge_kinds) if edge_kinds else "none"

            click.echo(f"Files: {total_files}  Symbols: {sym_count}  Edges: {edge_count}")
            click.echo(f"Languages: {lang_str}")
            click.echo(f"Edge kinds: {edge_str}")
            click.echo()

            dir_rows = [[d, str(c)] for d, c in (dir_items if full else dir_items[:15])]
            click.echo(section("Directories:", []))
            click.echo(
                format_table(
                    ["dir", "files"],
                    dir_rows,
                    budget=0 if full else 15,
                )
            )
            click.echo()

            if entries:
                click.echo("Entry points:")
                for e in entries if full else entries[:20]:
                    click.echo(f"  {e}")
                if not full and len(entries) > 20:
                    click.echo(f"  (+{len(entries) - 20} more)")
                click.echo()

        if top:
            rows = []
            for s in top:
                sig = format_signature(s["signature"], max_len=50)
                rows.append(
                    [
                        abbrev_kind(s["kind"]),
                        s["name"],
                        sig,
                        loc(s["file_path"], s["line_start"]),
                        f"{(s['pagerank'] or 0):.4f}",
                    ]
                )
            click.echo("Top symbols (PageRank):")
            click.echo(
                format_table(
                    ["kind", "name", "signature", "location", "PR"],
                    rows,
                )
            )
        else:
            click.echo("No graph metrics available. Run `roam index` first.")

        if budget is not None:
            click.echo()
            click.echo(f"Token budget: {tokens_used}/{budget} used")
