"""Show symbol definition, callers, and callees.

Output formats: text (default), ``--json``. SARIF is deliberately NOT
emitted because symbol outputs are invocation-scoped definition +
caller/callee enumerations — not per-location violations. See
action.yml _SUPPORTED_SARIF allowlist + W1175-RESEARCH Bucket B
propagation plan + W1148 audit memo.
"""

from __future__ import annotations

import click

from roam.capability import roam_capability
from roam.commands.resolve import ensure_index, find_symbol, symbol_not_found
from roam.db.connection import open_db
from roam.db.queries import (
    CALLEES_OF,
    CALLERS_OF,
    METRICS_FOR_SYMBOL,
)
from roam.output.formatter import (
    abbrev_kind,
    format_edge_kind,
    format_signature,
    json_envelope,
    loc,
    resolution_disclosure,
    section,
    to_json,
    truncate_lines,
)

# W543-followup: the priority tiers for ``inherits`` / ``implements`` /
# ``uses_trait`` mirror :data:`roam.db.edge_kinds.INHERITANCE_EDGE_KINDS`;
# ``import`` mirrors :data:`roam.db.edge_kinds.IMPORT_EDGE_KINDS` (plural
# ``'imports'`` collapses to the same priority via the default fallback).
# Keeping the mapping inline rather than computed off the canonical
# tuples preserves the explicit ordering: call < inherits < implements
# < uses_trait < import. ``uses_trait`` previously hit the default
# priority (1) — making it explicit means a future addition to the
# canonical inheritance set is a one-line change here, not a silent
# behaviour shift.
_EDGE_PRIORITY = {
    "call": 0,
    "template": 0,
    "inherits": 1,
    "implements": 2,
    "uses_trait": 2,
    "import": 3,
    "imports": 3,
}


def _dedup_edges(edges):
    """Dedup edges by symbol, preferring call > inherits > implements/uses_trait > import."""
    best = {}
    for c in edges:
        sid = c["id"]
        prio = _EDGE_PRIORITY.get(c["edge_kind"], 1)
        if sid not in best or prio < best[sid][1]:
            best[sid] = (c, prio)
    return [v[0] for v in best.values()]


@roam_capability(
    name="symbol",
    category="exploration",
    summary="Show symbol definition, callers, and callees",
    maturity="stable",
    mcp_expose=True,
    mcp_preset=("core",),
    side_effect=False,
    task_required=False,
    destructive=False,
    stale_sensitive=True,
    ai_safe=True,
    requires_index=True,
)
@click.command()
@click.argument("name", metavar="SYMBOL")
@click.option("--full", is_flag=True, help="Show all results without truncation")
@click.pass_context
def symbol(ctx, name, full):
    """Show definition, callers, and callees for SYMBOL.

    SYMBOL is a symbol identifier (bare name or qualified name). Unlike
    ``search`` (which finds symbols matching a pattern), this command
    shows detailed information about one symbol including callers,
    callees, and graph metrics.
    """
    json_mode = ctx.obj.get("json") if ctx.obj else False
    ensure_index()

    with open_db(readonly=True) as conn:
        s = find_symbol(conn, name)

        if s is None:
            # W1272 — Pattern-2c Convention (c): unresolved exits 0 with a
            # resolution=unresolved + partial_success disclosure. A
            # symbol lookup for a missing name is "I tried and there's
            # nothing to show" (a valid no-op success), not a tool
            # failure. Keep the FTS suggestion list in text mode.
            unresolved_block = resolution_disclosure("unresolved", target=name or "")
            if json_mode:
                click.echo(
                    to_json(
                        json_envelope(
                            "symbol",
                            summary={
                                "verdict": f"Symbol '{name}' not found",
                                "partial_success": True,
                                "state": "not_found",
                                **unresolved_block,
                            },
                            symbol=name or "",
                            **unresolved_block,
                        )
                    )
                )
            else:
                click.echo(symbol_not_found(conn, name, json_mode=False))
            return
        metrics = conn.execute(METRICS_FOR_SYMBOL, (s["id"],)).fetchone()
        callers = conn.execute(CALLERS_OF, (s["id"],)).fetchall()
        callees = conn.execute(CALLEES_OF, (s["id"],)).fetchall()
        deduped_callers = _dedup_edges(callers) if callers else []
        deduped_callees = _dedup_edges(callees) if callees else []

        # W1245 Pattern-2 variant-D: disclose which resolver tier matched
        # so MCP consumers can distinguish an exact-symbol-match success
        # from a fuzzy-LIKE-fallback "success" that landed on a different
        # symbol than the user requested.
        resolution_tier = s.get("_resolution_tier", "symbol")
        resolution_block = resolution_disclosure(resolution_tier, target=s["qualified_name"] or s["name"])
        fuzzy_suffix = " [fuzzy resolution]" if resolution_tier != "symbol" else ""

        if json_mode:
            _sym_loc = loc(s["file_path"], s["line_start"])
            _verdict = (
                f"{s['name']}: {abbrev_kind(s['kind'])} at {_sym_loc}, "
                f"{len(deduped_callers)} callers, {len(deduped_callees)} callees"
                f"{fuzzy_suffix}"
            )
            data = {
                "name": s["qualified_name"] or s["name"],
                "kind": s["kind"],
                "signature": s["signature"] or "",
                "location": _sym_loc,
                "docstring": s["docstring"] or "",
            }
            if metrics:
                data["pagerank"] = round(metrics["pagerank"], 4)
                data["in_degree"] = metrics["in_degree"]
                data["out_degree"] = metrics["out_degree"]
            data["callers"] = [
                {
                    "name": c["name"],
                    "kind": c["kind"],
                    "edge_kind": c["edge_kind"],
                    "location": loc(c["file_path"], c["edge_line"]),
                }
                for c in deduped_callers
            ]
            data["callees"] = [
                {
                    "name": c["name"],
                    "kind": c["kind"],
                    "edge_kind": c["edge_kind"],
                    "location": loc(c["file_path"], c["edge_line"]),
                }
                for c in deduped_callees
            ]
            click.echo(
                to_json(
                    json_envelope(
                        "symbol",
                        summary={
                            "verdict": _verdict,
                            "callers": len(deduped_callers),
                            "callees": len(deduped_callees),
                            "caller_metric_definition": "direct_in_degree",
                            # W1245 Pattern-2 variant-D resolution disclosure.
                            **resolution_block,
                        },
                        **data,
                        **resolution_block,
                    )
                )
            )
            return

        # --- Text output ---
        _sym_loc = loc(s["file_path"], s["line_start"])
        _verdict = (
            f"{s['name']}: {abbrev_kind(s['kind'])} at {_sym_loc}, "
            f"{len(deduped_callers)} callers, {len(deduped_callees)} callees"
        )
        click.echo(f"VERDICT: {_verdict}\n")
        sig = format_signature(s["signature"])
        click.echo(f"{abbrev_kind(s['kind'])}  {s['qualified_name'] or s['name']}")
        if sig:
            click.echo(f"  {sig}")
        click.echo(f"  {loc(s['file_path'], s['line_start'])}")

        if s["docstring"]:
            doc_lines = s["docstring"].strip().splitlines()
            if not full:
                doc_lines = truncate_lines(doc_lines, 5)
            for dl in doc_lines:
                click.echo(f"  | {dl}")

        if metrics:
            click.echo(f"  PR={metrics['pagerank']:.4f}  in={metrics['in_degree']}  out={metrics['out_degree']}")

        if deduped_callers:
            lines = []
            for c in deduped_callers:
                edge = format_edge_kind(c["edge_kind"])
                lines.append(
                    f"  {abbrev_kind(c['kind'])}  {c['name']}  ({edge})  {loc(c['file_path'], c['edge_line'])}"
                )
            click.echo(section(f"Callers ({len(deduped_callers)}):", lines, budget=0 if full else 15))

        if deduped_callees:
            lines = []
            for c in deduped_callees:
                edge = format_edge_kind(c["edge_kind"])
                lines.append(
                    f"  {abbrev_kind(c['kind'])}  {c['name']}  ({edge})  {loc(c['file_path'], c['edge_line'])}"
                )
            click.echo(section(f"Callees ({len(deduped_callees)}):", lines, budget=0 if full else 15))
