"""Plotly figure builders — presentation only, consuming engine outputs.

Chart chrome follows the house dataviz rules (see the repo dataviz
reference and docs/brand.md): recessive grid and axes, ink text tokens,
fixed entity→color assignments so a color means one thing across every
figure, 2px surface gaps between adjacent fills, selective direct labels,
one axis per chart. Room-facing: sized and typed to hold attention on a
shared screen, not for print.
"""

from __future__ import annotations

import plotly.graph_objects as go

from .models import DiagnosticConfig, ValueAtStake
from .value_math import html_money

# ── Chrome (dataviz reference palette, light surface) ────────────────
INK, INK2, MUTED = "#0b0b0b", "#52514e", "#898781"
SURFACE, GRID, BASELINE = "#fcfcfb", "#e1e0d9", "#c3c2b7"
FONT_STACK = 'system-ui, -apple-system, "Segoe UI", sans-serif'

# Fixed entity colors — color follows the entity across every figure.
THEORETICAL = "#9ec5f4"   # light blue: the outer envelope
REALIZABLE = "#2a78d6"    # blue: what capability can actually capture
TRAPPED = "#eda100"       # amber: value the capability gap strands
COST = "#e34948"          # red: unlock investment
UNLOCKED = "#1baf7a"      # aqua-green: unlock payoff

# Diverging maturity scale, centered on level 3 — soft poles so ink text
# stays readable in every cell (two hues + neutral midpoint, never rainbow).
SCORE_SCALE = [
    (0.0, "#ef8a76"), (0.5, "#f0efe9"), (1.0, "#57c993"),
]


def diag_layout(fig: go.Figure, title: str, subtitle: str | None = None,
                height: int = 440) -> go.Figure:
    t = f"<b>{title}</b>"
    if subtitle:
        t += f"<br><span style='font-size:12px;color:{MUTED}'>{subtitle}</span>"
    fig.update_layout(
        title=dict(text=t, font=dict(size=16, color=INK), x=0.02, xanchor="left"),
        paper_bgcolor=SURFACE, plot_bgcolor=SURFACE,
        font=dict(family=FONT_STACK, size=13, color=INK2),
        legend=dict(orientation="h", yanchor="top", y=-0.12, x=0,
                    font=dict(size=11, color=INK2)),
        height=height, margin=dict(t=70, r=30, b=60, l=70),
    )
    return fig


# ── 1 · Capability heatmap (4 dimensions × 3 competencies) ───────────


def heatmap_fig(grid: dict) -> go.Figure:
    """``grid`` comes from scoring.heatmap_grid — pure data in."""
    n_rows = len(grid["rows"])
    fig = go.Figure(go.Heatmap(
        z=grid["z"], text=grid["text"], customdata=grid["hover"],
        x=list(range(len(grid["cols"]))), y=grid["rows"],
        zmin=1, zmax=5, colorscale=SCORE_SCALE, showscale=False,
        texttemplate="%{text}", textfont=dict(size=13, color=INK),
        hovertemplate="%{customdata}<extra></extra>",
        xgap=3, ygap=3,
    ))
    fig.update_xaxes(visible=False)
    fig.update_yaxes(autorange="reversed", tickfont=dict(size=13, color=INK2),
                     showgrid=False)
    diag_layout(fig, "Capability heatmap",
                "12 competencies, levels 1–5 · hover a cell for the evidence",
                height=90 * n_rows + 120)
    return fig


# ── 2 · Value at stake (theoretical vs realizable ranges) ────────────


def value_bands_fig(vas: ValueAtStake) -> go.Figure:
    rows = [
        ("Theoretical value (annual)", vas.theoretical_annual_value_low,
         vas.theoretical_annual_value_high, THEORETICAL),
        ("Realizable over 18 months", vas.realizable_18mo_low,
         vas.realizable_18mo_high, REALIZABLE),
    ]
    fig = go.Figure()
    for label, low, high, color in rows:
        fig.add_trace(go.Bar(
            y=[label], x=[max(high - low, 1)], base=[low], orientation="h",
            marker=dict(color=color, line=dict(width=2, color=SURFACE)),
            showlegend=False,
            hovertemplate=(f"{label}: {html_money(low)} – {html_money(high)}"
                           "<extra></extra>"),
        ))
        fig.add_annotation(x=high, y=label, xanchor="left", xshift=6,
                           text=f"{html_money(low)} – {html_money(high)}",
                           showarrow=False, font=dict(size=12, color=INK))
    fig.add_annotation(
        xref="paper", yref="paper", x=0.02, y=-0.32, xanchor="left",
        showarrow=False, align="left",
        text=(f"Trapped by the capability gap: "
              f"<b>{html_money(vas.trapped_value_low)} – "
              f"{html_money(vas.trapped_value_high)}</b> per year"),
        font=dict(size=13, color=INK))
    fig.update_xaxes(tickformat="$~s", gridcolor=GRID, zeroline=False,
                     tickfont=dict(color=MUTED), rangemode="tozero")
    fig.update_yaxes(tickfont=dict(size=13, color=INK2), showgrid=False,
                     autorange="reversed")   # theoretical on top, then realizable
    diag_layout(fig, "Value at stake",
                "ranges, never points · realization capped by the weakest foundation",
                height=300)
    fig.update_layout(margin=dict(b=90), bargap=0.5)
    return fig


# ── 3 · Realizable vs trapped split (scenario-consistent) ────────────


def split_fig(vas: ValueAtStake) -> go.Figure:
    """Each scenario bar splits its own theoretical total: realizable
    run-rate vs trapped, at that scenario's realization factor."""
    scenarios = [
        ("Conservative", vas.theoretical_annual_value_low, vas.realization_factor_low),
        ("Optimistic", vas.theoretical_annual_value_high, vas.realization_factor_high),
    ]
    labels = [s[0] for s in scenarios]
    realizable = [th * r for _, th, r in scenarios]
    trapped = [th * (1 - r) for _, th, r in scenarios]
    fig = go.Figure([
        go.Bar(name="Realizable (annual run-rate)", y=labels, x=realizable,
               orientation="h",
               marker=dict(color=REALIZABLE, line=dict(width=2, color=SURFACE)),
               text=[html_money(v) for v in realizable],
               textposition="inside", insidetextfont=dict(color="#ffffff"),
               hovertemplate="Realizable: %{x:$,.0f}<extra>%{y}</extra>"),
        go.Bar(name="Trapped by capability gap", y=labels, x=trapped,
               orientation="h",
               marker=dict(color=TRAPPED, line=dict(width=2, color=SURFACE)),
               text=[html_money(v) for v in trapped],
               textposition="inside", insidetextfont=dict(color=INK),
               hovertemplate="Trapped: %{x:$,.0f}<extra>%{y}</extra>"),
    ])
    fig.update_layout(barmode="stack", bargap=0.5)
    fig.update_xaxes(tickformat="$~s", gridcolor=GRID, zeroline=False,
                     tickfont=dict(color=MUTED))
    fig.update_yaxes(tickfont=dict(size=13, color=INK2), showgrid=False,
                     autorange="reversed")   # conservative on top
    diag_layout(fig, "Where the annual value goes",
                "each scenario splits its own theoretical total", height=300)
    fig.update_layout(legend=dict(traceorder="normal"))
    return fig


# ── 4 · Unlock sequence (cost vs value per move) ─────────────────────


def _move_label(vas_move, config: DiagnosticConfig, idx: int) -> str:
    """Compact tick label — full competency names live in the hover and
    in the on-stage moves table (long joint-lift names don't fit ticks)."""
    if len(vas_move.competency_ids) == 1:
        what = config.competency(vas_move.competency_ids[0]).name
    else:
        what = f"joint lift ×{len(vas_move.competency_ids)}"
    return (f"<b>{idx} · {what}</b><br>"
            f"level {vas_move.current_level} → {vas_move.target_level}")


def unlock_fig(vas: ValueAtStake, config: DiagnosticConfig) -> go.Figure:
    moves = vas.unlock_sequence
    labels = [_move_label(m, config, i + 1) for i, m in enumerate(moves)]
    names = [" + ".join(config.competency(c).name for c in m.competency_ids)
             for m in moves]
    fig = go.Figure()
    fig.add_trace(go.Bar(
        name="Investment (range)", x=labels,
        y=[(m.est_cost_high - m.est_cost_low) if m.est_cost_low is not None else 0
           for m in moves],
        base=[m.est_cost_low if m.est_cost_low is not None else 0 for m in moves],
        customdata=[[n, html_money(m.est_cost_low) + " – " + html_money(m.est_cost_high)
                     if m.est_cost_low is not None else "not configured"]
                    for n, m in zip(names, moves)],
        marker=dict(color=COST, line=dict(width=2, color=SURFACE)),
        hovertemplate="%{customdata[0]}<br>Investment: %{customdata[1]}<extra></extra>",
    ))
    fig.add_trace(go.Bar(
        name="Annual value unlocked (range)", x=labels,
        y=[m.value_unlocked_high - m.value_unlocked_low for m in moves],
        base=[m.value_unlocked_low for m in moves],
        customdata=[[n, html_money(m.value_unlocked_low) + " – "
                     + html_money(m.value_unlocked_high)]
                    for n, m in zip(names, moves)],
        marker=dict(color=UNLOCKED, line=dict(width=2, color=SURFACE)),
        hovertemplate="%{customdata[0]}<br>Unlocked: %{customdata[1]}<extra></extra>",
    ))
    for i, m in enumerate(moves):
        if m.est_cost_low is None:
            fig.add_annotation(x=labels[i], y=0, yanchor="bottom",
                               text="cost not<br>configured", showarrow=False,
                               font=dict(size=11, color=MUTED))
    fig.update_layout(barmode="group", bargap=0.35, bargroupgap=0.12)
    fig.update_xaxes(tickfont=dict(size=12, color=INK2), showgrid=False,
                     tickangle=0)
    fig.update_yaxes(tickformat="$~s", gridcolor=GRID, zerolinecolor=BASELINE,
                     tickfont=dict(color=MUTED))
    diag_layout(fig, "Unlock sequence",
                "sequential moves — each is the prerequisite of the next",
                height=420)
    return fig
