#
#   Imandra Inc.
#
#   speclogician/presentation/renderers/trace_reachability.py
#

from __future__ import annotations

from rich import box
from rich.console import Group
from rich.panel import Panel
from rich.rule import Rule
from rich.table import Table
from rich.text import Text

from speclogician.presentation.ctx import RenderCtx
from speclogician.presentation.models.trace_reachability import TraceReachabilityPM, TraceReachabilityTracePM


def _counts_table(pm: TraceReachabilityPM, *, ctx: RenderCtx) -> Table:
    b = box.MINIMAL if ctx.compact else box.SIMPLE
    t = Table(show_header=False, box=b, show_edge=False, pad_edge=False)
    t.add_column(style="italic dim", no_wrap=True)
    t.add_column(justify="right", no_wrap=True)

    c = pm.counts

    title = "Trace reachability"
    if getattr(ctx, "trace_id", None):
        title = f"Trace reachability (trace={ctx.trace_id})"

    t.add_row(Text(title, style="bold magenta"), Text(""))
    t.add_row("Traces", Text(str(c.total_traces), style="bold"))
    t.add_row("Steps", Text(str(c.total_steps)))
    t.add_row("Hops", Text(str(c.total_hops)))
    t.add_row("Supported hops", Text(str(c.supported_hops), style="green"))
    t.add_row("Unsupported hops", Text(str(c.unsupported_hops), style="red"))
    t.add_row("Scenarios hit", Text(str(c.scenarios_hit)))
    t.add_row("Scenarios unhit", Text(str(c.scenarios_unhit)))
    return t


def _top_issues_table(pm: TraceReachabilityPM, *, ctx: RenderCtx) -> Table:
    b = box.MINIMAL if ctx.compact else box.SIMPLE
    t = Table(show_header=False, box=b, show_edge=False, pad_edge=False)
    t.add_column(style="italic dim", no_wrap=True)
    t.add_column(overflow="fold")

    s = pm.summary

    t.add_row(Text("Top issues", style="bold magenta"), Text(""))

    if s.top_unsupported_edges:
        edges = ", ".join([f"{a}→{b} ({n})" for (a, b, n) in s.top_unsupported_edges[:5]])
    else:
        edges = "—"
    t.add_row("Unsupported edges", Text(edges))

    if s.top_frontier_trace_endings:
        fr = ", ".join([f"{sc} ({n})" for (sc, n) in s.top_frontier_trace_endings[:5]])
    else:
        fr = "—"
    t.add_row("Frontier endings", Text(fr))

    return t


def _trace_list_table(pm: TraceReachabilityPM, *, ctx: RenderCtx) -> Table:
    b = box.MINIMAL if ctx.compact else box.SIMPLE

    title = "Traces"
    tid = getattr(ctx, "trace_id", None)
    if tid:
        title = f"Trace: {tid}"

    t = Table(title=title, box=b, show_edge=False, pad_edge=False, expand=False)
    t.add_column("Trace", no_wrap=True)
    t.add_column("Kind", justify="right", no_wrap=True)
    t.add_column("Steps", justify="right", no_wrap=True)
    t.add_column("Matched", justify="right", no_wrap=True)
    t.add_column("Supported", justify="right", no_wrap=True)
    t.add_column("Unsupported", justify="right", no_wrap=True)
    t.add_column("Best path", overflow="fold")

    traces = list(pm.traces or [])
    if tid:
        traces = [r for r in traces if r.trace_id == tid]

    if not traces:
        t.add_row(Text("(none)", style="dim"), "", "", "", "", "", "")
        return t

    def chain(step_best: list[str | None], max_steps: int = 12) -> str:
        xs = [(x if x is not None else "·") for x in (step_best or [])]
        if len(xs) > max_steps:
            return " → ".join(xs[:max_steps]) + " → …"
        return " → ".join(xs)

    for r in traces:
        t.add_row(
            Text(r.trace_label),
            Text(r.trace_kind, style="dim"),
            Text(str(r.num_steps)),
            Text(str(r.num_matched_steps)),
            Text(str(r.num_supported_hops), style="green" if r.num_supported_hops else ""),
            Text(str(r.num_unsupported_hops), style="red" if r.num_unsupported_hops else ""),
            Text(chain(r.step_best), style="dim"),
        )

    return t


def _unhit_preview(pm: TraceReachabilityPM, *, ctx: RenderCtx) -> Text:
    xs = pm.summary.scenarios_unhit_preview or []
    if not xs:
        return Text("(none)", style="dim")
    s = ", ".join(xs)
    if pm.counts.scenarios_unhit > len(xs):
        s += " ..."
    return Text(s, style="yellow")


def _unsupported_hops_table(
    tr: TraceReachabilityTracePM,
    *,
    ctx: RenderCtx,
    max_rows: int = 10,
) -> Table:
    b = box.MINIMAL if ctx.compact else box.SIMPLE
    t = Table(title="Unsupported hops (details)", box=b, show_edge=False, pad_edge=False, expand=False)
    t.add_column("#", justify="right", no_wrap=True)
    t.add_column("From→To", no_wrap=True)
    t.add_column("Reason", no_wrap=True)
    t.add_column("Shortest path", overflow="fold")

    hops = list(tr.unsupported_hops or [])
    if not hops:
        t.add_row(Text("—", style="dim"), Text("(none)", style="dim"), "", "")
        return t

    for i, h in enumerate(hops[:max_rows], 1):
        src = h.src or "·"
        dst = h.dst or "·"
        reason = h.reason or "—"
        sp = ""
        if h.shortest_path:
            sp = " → ".join(h.shortest_path)
        t.add_row(
            Text(str(i), style="dim"),
            Text(f"{src}→{dst}"),
            Text(reason, style="red" if reason in ("no_graph_edge", "no_path") else "dim"),
            Text(sp, style="dim" if sp else "dim"),
        )

    if len(hops) > max_rows:
        t.add_row(Text("…", style="dim"), Text(f"(+{len(hops)-max_rows} more)", style="dim"), "", "")

    return t


def _single_trace_detail_panel(pm: TraceReachabilityPM, *, ctx: RenderCtx) -> Panel | None:
    """
    If ctx.trace_id is set and we have that trace in pm.traces, show drilldown:
    - step_best chain
    - unsupported hops table (capped)
    """
    tid = getattr(ctx, "trace_id", None)
    if not tid:
        return None

    trace = next((t for t in (pm.traces or []) if t.trace_id == tid), None)
    if trace is None:
        return None

    def chain(step_best: list[str | None], max_steps: int = 40) -> str:
        xs = [(x if x is not None else "·") for x in (step_best or [])]
        if len(xs) > max_steps:
            return " → ".join(xs[:max_steps]) + " → …"
        return " → ".join(xs)

    blocks: list[object] = []
    blocks.append(Text("Best path", style="bold magenta"))
    blocks.append(Text(chain(trace.step_best), style="dim"))
    blocks.append(Rule(style="dim"))
    blocks.append(_unsupported_hops_table(trace, ctx=ctx, max_rows=10))

    return Panel(Group(*blocks), title="[bold]Trace details[/bold]", border_style="yellow")


def render_trace_reachability(pm: TraceReachabilityPM, *, ctx: RenderCtx):
    blocks: list[object] = []

    blocks.append(_counts_table(pm, ctx=ctx))
    blocks.append(Rule(style="dim"))
    blocks.append(_top_issues_table(pm, ctx=ctx))

    if pm.counts.scenarios_unhit or ctx.show_empty_sections:
        blocks.append(Rule(style="dim"))
        blocks.append(Text("Unhit scenarios", style="bold magenta"))
        blocks.append(_unhit_preview(pm, ctx=ctx))

    # If the user asked for a specific trace, include drilldown panel.
    detail = _single_trace_detail_panel(pm, ctx=ctx)
    if detail is not None and not ctx.show_stats_only:
        blocks.append(Rule(style="dim"))
        blocks.append(detail)

    # In stats-only mode, skip the potentially large per-trace table.
    if not ctx.show_stats_only:
        blocks.append(Rule(style="dim"))
        blocks.append(_trace_list_table(pm, ctx=ctx))

    return Panel(Group(*blocks), title="[bold]Trace reachability[/bold]", border_style="magenta")