"""Pipeline diagram: attention + FFN steps, colored by bound type.

Each stage is drawn as a rounded box in a horizontal flow:
  Attention: S1 -> S2 -> S3 -> ... -> S10 -> C2 (AllReduce)
  FFN:       F1 -> F2 -> F3 -> F4 -> F5 -> CF1 (AllReduce)

CP ring communication (C1) and score AllReduce (C3) are drawn as
"overhead" boxes attached above the QKt/PV region (they run
concurrently with per-hop compute in the ring).

Box fill color = the stage's bound classification:
  compute-bound  blue
  memory-bound   orange
  comm-bound     red
  trivial        grey
"""
from __future__ import annotations

import matplotlib.pyplot as plt
import matplotlib.patches as patches

from .model_config import FullConfig
from .stage_latencies import (
    all_stages, all_ffn_stages, StageCost,
)


BOUND_COLOR = {
    "compute": "#4682b4",   # blue
    "memory":  "#e37400",   # orange
    "comm":    "#c62828",   # red
    "trivial": "#a9a9a9",   # grey
}
BOUND_LABEL = {
    "compute": "compute-bound",
    "memory":  "memory-bound",
    "comm":    "communication",
    "trivial": "trivial / negligible",
}


def _fmt_us(sec: float) -> str:
    us = sec * 1e6
    if us < 1:
        return f"{us*1e3:.1f}ns"
    if us < 100:
        return f"{us:.2f}us"
    return f"{us:.0f}us"


def _draw_block(ax, x, y, w, h, label, cost: StageCost, edge="#333"):
    color = BOUND_COLOR.get(cost.bound, "#a9a9a9")
    box = patches.FancyBboxPatch(
        (x, y), w, h,
        boxstyle="round,pad=0.05",
        facecolor=color, edgecolor=edge, linewidth=1.2, alpha=0.85,
    )
    ax.add_patch(box)
    # Two lines: stage name (bold) + visible latency (small)
    ax.text(x + w / 2, y + h * 0.62, label,
            ha="center", va="center",
            fontsize=8.0, fontweight="bold", color="white")
    ax.text(x + w / 2, y + h * 0.25, _fmt_us(cost.visible_s),
            ha="center", va="center",
            fontsize=7.0, color="white")


_SHORT_NAMES = {
    "S1":  "RMSNorm",
    "S2":  "W_Q",
    "S3":  "W_K+W_V",
    "S4":  "KV append",
    "S5":  "Q.K^T",
    "S6":  "softmax",
    "S7":  "P.V",
    "S8":  "merge",
    "S9":  "norm O/l",
    "S10": "W_O",
    "C1":  "CP ring",
    "C2":  "TP AR",
    "C3":  "Score AR",
    "F1":  "RMSNorm",
    "F2":  "W_gate",
    "F3":  "W_up",
    "F4":  "SwiGLU",
    "F5":  "W_down",
    "CF1": "FFN AR",
}


def _short_name(name: str) -> str:
    """Map 'S1 RMSNorm' -> 'RMSNorm'; fallback to first token."""
    tok = name.split()[0]
    return _SHORT_NAMES.get(tok, tok)


def _draw_arrow(ax, x0, x1, y):
    ax.annotate("", xy=(x1, y), xytext=(x0, y),
                arrowprops=dict(arrowstyle="->", color="#333", lw=1.1))


def _draw_ring_loop(ax, x_left, x_right, y_top, label: str, n_iters: int,
                     arc_lift: float = 0.9):
    """Draw a curved back-arrow above [x_left..x_right] with a repeat label.

    Visualizes 'repeat this block n_iters times' (like a loop in a diagram).
    arc_lift: how high above y_top the arc sits.
    """
    color = "#5a189a"
    lw = 1.6
    arc_y = y_top + arc_lift
    # Vertical tick up from the right edge
    ax.plot([x_right, x_right], [y_top + 0.02, arc_y],
            color=color, linewidth=lw)
    # Horizontal top of the loop
    ax.plot([x_right, x_left], [arc_y, arc_y],
            color=color, linewidth=lw)
    # Down-arrow into the left edge (a back-edge indicating the repeat)
    ax.annotate("", xy=(x_left, y_top + 0.02), xytext=(x_left, arc_y),
                arrowprops=dict(arrowstyle="->", color=color, lw=lw))
    # Loop label centered above the arc
    ax.text((x_left + x_right) / 2, arc_y + 0.08,
            label,
            ha="center", va="bottom",
            fontsize=9, fontweight="bold", color=color)
    # Iteration count as a badge on the right
    ax.text(x_right + 0.08, arc_y - 0.03,
            f"x {n_iters}",
            ha="left", va="top",
            fontsize=10, fontweight="bold", color=color,
            bbox=dict(boxstyle="round,pad=0.15", facecolor="white",
                      edgecolor=color, linewidth=1.2, alpha=0.95))


def _draw_row(ax, y_base, row_title, stages: list[StageCost],
              x_start: float, box_w: float, box_h: float, gap: float):
    """Draw one row of stage boxes with arrows between them."""
    ax.text(x_start - 0.4, y_base + box_h / 2, row_title,
            ha="right", va="center",
            fontsize=11, fontweight="bold", color="#212529")
    x = x_start
    for i, st in enumerate(stages):
        _draw_block(ax, x, y_base, box_w, box_h,
                    _short_name(st.name), st)
        if i < len(stages) - 1:
            _draw_arrow(ax, x + box_w + 0.02, x + box_w + gap - 0.02,
                        y_base + box_h / 2)
        x += box_w + gap
    return x - gap


def _draw_overlap_boxes(ax, hidden_stages, x_left, x_right, y_top,
                          gap_between: float = 0.15):
    """Draw dashed 'overlapped comm' boxes spanning x_left..x_right above y_top.

    Boxes are widened to cover the horizontal range of the stages they
    overlap with (e.g. C1 CP ring sits above S5-S7).
    """
    if not hidden_stages:
        return
    hy = y_top + 0.10
    n = len(hidden_stages)
    total_span = x_right - x_left
    box_span = (total_span - (n - 1) * gap_between) / max(1, n)
    box_h_local = 0.42
    hx = x_left
    for st in hidden_stages:
        box = patches.FancyBboxPatch(
            (hx, hy), box_span, box_h_local,
            boxstyle="round,pad=0.03",
            facecolor=BOUND_COLOR["comm"], edgecolor="#333",
            linewidth=1.0, alpha=0.35, linestyle="--",
        )
        ax.add_patch(box)
        ax.text(hx + box_span / 2, hy + box_h_local * 0.65,
                f"{_short_name(st.name)} (concurrent)",
                ha="center", va="center",
                fontsize=7, color="#7a0e0e", fontweight="bold")
        ax.text(hx + box_span / 2, hy + box_h_local * 0.2,
                _fmt_us(st.visible_s),
                ha="center", va="center",
                fontsize=6.5, color="#7a0e0e")
        hx += box_span + gap_between


def draw_pipeline(cfg: FullConfig, ax=None):
    """Draw attention + FFN pipeline. Boxes colored by bound type."""
    attn = all_stages(cfg)
    ffn = all_ffn_stages(cfg)

    # Split attention: main stages, then C2 (TP AllReduce). C1/C3 = overlapped.
    attn_main = [s for s in attn if not s.name.startswith("C")]
    attn_c2 = next((s for s in attn if s.name.startswith("C2")), None)
    attn_hidden = [s for s in attn
                   if s.name.startswith("C1") or s.name.startswith("C3")]
    attn_hidden = [s for s in attn_hidden if s.visible_s > 0]
    attn_row = attn_main + ([attn_c2] if attn_c2 and attn_c2.visible_s > 0 else [])

    # FFN row: all F* + CF1 if non-trivial
    ffn_main = [s for s in ffn if not s.name.startswith("CF")]
    ffn_cf = next((s for s in ffn if s.name.startswith("CF1")), None)
    ffn_row = ffn_main + ([ffn_cf] if ffn_cf and ffn_cf.visible_s > 0 else [])

    n_max = max(len(attn_row), len(ffn_row))
    box_w = 1.35
    box_h = 0.78
    gap = 0.18
    x_start = 1.1

    total_w = x_start + n_max * box_w + (n_max - 1) * gap + 0.5
    total_h = 4.2

    if ax is None:
        fig, ax = plt.subplots(figsize=(max(11, total_w * 1.1),
                                         max(4.5, total_h * 1.0)))
    else:
        fig = ax.figure

    _draw_row(ax, y_base=2.6, row_title="Attention",
              stages=attn_row, x_start=x_start,
              box_w=box_w, box_h=box_h, gap=gap)

    _draw_row(ax, y_base=0.6, row_title="FFN",
              stages=ffn_row, x_start=x_start,
              box_w=box_w, box_h=box_h, gap=gap)

    # Ring loop indicator + concurrent-comm boxes anchored over S5..S8.
    _idx_by_prefix = {s.name.split()[0]: i for i, s in enumerate(attn_row)}
    _s5_idx = _idx_by_prefix.get("S5")
    _s7_idx = _idx_by_prefix.get("S7")
    _s8_idx = _idx_by_prefix.get("S8")

    if cfg.topo.cp > 1 and cfg.topo.mode == "prefill" and _s5_idx is not None:
        _right_idx = _s8_idx if _s8_idx is not None else _s7_idx
        _lx = x_start + _s5_idx * (box_w + gap)
        _rx = x_start + _right_idx * (box_w + gap) + box_w
        _y_top = 2.6 + box_h
        # Concurrent-comm boxes (C1 CP ring, C3 score AR) directly above S5-S7
        _draw_overlap_boxes(ax, attn_hidden, _lx, _rx, _y_top)
        # Ring loop arc above those
        _draw_ring_loop(
            ax, _lx, _rx, _y_top,
            label="ring attention loop (K/V ring per hop; S8 merges partial O,m,l)",
            n_iters=cfg.topo.cp,
            arc_lift=1.05,
        )
    elif cfg.topo.cp > 1 and cfg.topo.mode == "decode" and _s5_idx is not None:
        _right_idx = _s7_idx if _s7_idx is not None else _s5_idx
        _lx = x_start + _s5_idx * (box_w + gap)
        _rx = x_start + _right_idx * (box_w + gap) + box_w
        _y_top = 2.6 + box_h
        # Any straggling concurrent comm (e.g. C3 if head-split active) above S5-S7
        _draw_overlap_boxes(ax, attn_hidden, _lx, _rx, _y_top)
        _draw_ring_loop(
            ax, _lx, _rx, _y_top,
            label="decode: local pass only; S8 fires O/m/l all-reduce",
            n_iters=1,
            arc_lift=1.05 if attn_hidden else 0.85,
        )
    elif attn_hidden:
        # Fallback: CP=1 but still have C3 or similar; anchor above S5-S7 if present
        if _s5_idx is not None and _s7_idx is not None:
            _lx = x_start + _s5_idx * (box_w + gap)
            _rx = x_start + _s7_idx * (box_w + gap) + box_w
            _draw_overlap_boxes(ax, attn_hidden, _lx, _rx, 2.6 + box_h)

    # Totals
    attn_total = sum(s.visible_s for s in attn_row) \
        + sum(s.visible_s for s in attn_hidden)
    ffn_total = sum(s.visible_s for s in ffn_row)
    ax.text(total_w - 0.3, 2.6 + box_h / 2 + 1.15,
            f"Attn total: {_fmt_us(attn_total)}",
            ha="right", va="bottom", fontsize=9,
            fontweight="bold", color="#212529")
    ax.text(total_w - 0.3, 0.6 + box_h + 0.15,
            f"FFN total:  {_fmt_us(ffn_total)}",
            ha="right", va="bottom", fontsize=9,
            fontweight="bold", color="#212529")

    # Legend
    handles = []
    for k, lbl in BOUND_LABEL.items():
        handles.append(patches.Patch(
            facecolor=BOUND_COLOR[k], edgecolor="#333",
            alpha=0.85, label=lbl,
        ))
    ax.legend(handles=handles, loc="upper center",
              bbox_to_anchor=(0.5, -0.02),
              ncol=4, fontsize=8, frameon=False)

    ax.set_xlim(0, total_w + 0.6)
    ax.set_ylim(0, 4.9)
    ax.set_aspect("auto")
    ax.axis("off")
    ax.set_title(
        f"Per-layer pipeline (one PE, {cfg.topo.mode}, T_q={cfg.topo.T_q}, "
        f"S_local={cfg.topo.s_local}) - color = dominant bound",
        fontsize=10, fontweight="bold",
    )
    return fig
