"""Rendering helpers for explorer tabs.

This module contains all Plotly/Streamlit rendering functions extracted from
explorer.py.  It is import-safe: plotly and streamlit are optional.
"""

from __future__ import annotations

import json
import logging
from typing import Dict, List, Optional, Tuple

try:
    import plotly.express as px
    import plotly.graph_objects as go
except Exception:
    px = None
    import types

    class _DummyTrace:
        def __init__(self, **kwargs):
            self.name = kwargs.get("name")
            self.x = kwargs.get("x")
            self.y = kwargs.get("y")
            self.text = kwargs.get("text")
            self.customdata = kwargs.get("customdata")

    class _DummyFigure:
        def __init__(self):
            self.data = []

        def add_trace(self, trace):
            if isinstance(trace, _DummyTrace):
                self.data.append(trace)
            else:
                try:
                    name = getattr(trace, "name", None)
                    x = getattr(trace, "x", None)
                    y = getattr(trace, "y", None)
                    text = getattr(trace, "text", None)
                    customdata = getattr(trace, "customdata", None)
                except Exception:
                    name = trace.get("name") if hasattr(trace, "get") else None
                    x = trace.get("x") if hasattr(trace, "get") else None
                    y = trace.get("y") if hasattr(trace, "get") else None
                    text = trace.get("text") if hasattr(trace, "get") else None
                    customdata = (
                        trace.get("customdata") if hasattr(trace, "get") else None
                    )
                self.data.append(
                    _DummyTrace(name=name, x=x, y=y, text=text, customdata=customdata)
                )

        def add_annotation(self, *args, **kwargs):
            return None

        def update_layout(self, **kwargs):
            return None

        def update_traces(self, **kwargs):
            return None

        def add_hline(self, **kwargs):
            return None

    go = types.SimpleNamespace(
        Figure=_DummyFigure,
        Scatter=lambda **kwargs: _DummyTrace(**kwargs),
        Bar=lambda **kwargs: _DummyTrace(**kwargs),
    )

try:
    import streamlit as st
except Exception:

    class _DummySt:
        def cache_data(self, *args, **kwargs):
            def _decorator(func):
                return func

            return _decorator

        def markdown(self, *args, **kwargs):
            return None

        def subheader(self, *args, **kwargs):
            return None

        def plotly_chart(self, *args, **kwargs):
            return None

        def caption(self, *args, **kwargs):
            return None

        def text_area(self, *args, **kwargs):
            return None

        def json(self, *args, **kwargs):
            return None

        def checkbox(self, *args, **kwargs):
            return kwargs.get("value", False)

        def warning(self, *args, **kwargs):
            return None

        def info(self, *args, **kwargs):
            return None

        def error(self, *args, **kwargs):
            return None

        def success(self, *args, **kwargs):
            return None

        def selectbox(self, *args, **kwargs):
            opts = (
                kwargs.get("options")
                if kwargs.get("options") is not None
                else (args[1] if len(args) > 1 else [])
            )
            return opts[0] if opts else None

        def multiselect(self, *args, **kwargs):
            opts = (
                kwargs.get("options")
                if kwargs.get("options") is not None
                else (args[1] if len(args) > 1 else [])
            )
            default = kwargs.get("default")
            if default is not None:
                return default
            return opts[:6] if opts else []

        def number_input(self, *args, **kwargs):
            return kwargs.get("value") if "value" in kwargs else 1

        def slider(self, *args, **kwargs):
            return kwargs.get("value") if "value" in kwargs else 0.35

        def select_slider(self, *args, **kwargs):
            return kwargs.get("value") if "value" in kwargs else (None, None)

        def expander(self, *args, **kwargs):
            class _Ctx:
                def __enter__(self_inner):
                    return self_inner

                def __exit__(self_inner, exc_type, exc, tb):
                    return False

            return _Ctx()

        def columns(self, *args, **kwargs):
            class _Col:
                def markdown(self, *a, **k):
                    return None

                def metric(self, *a, **k):
                    return None

                def dataframe(self, *a, **k):
                    return None

                def write(self, *a, **k):
                    return None

                def text_input(self, *a, **k):
                    return None

            n = len(args[0]) if args else 1
            return tuple(_Col() for _ in range(n))

        def form(self, *args, **kwargs):
            class _Ctx:
                def __enter__(self_inner):
                    return self_inner

                def __exit__(self_inner, exc_type, exc, tb):
                    return False

            return _Ctx()

        def form_submit_button(self, *args, **kwargs):
            return False

        def button(self, *args, **kwargs):
            return False

        def rerun(self, *args, **kwargs):
            return None

        def divider(self, *args, **kwargs):
            return None

        def spinner(self, *args, **kwargs):
            class _Ctx:
                def __enter__(self_inner):
                    return self_inner

                def __exit__(self_inner, exc_type, exc, tb):
                    return False

            return _Ctx()

        def write(self, *args, **kwargs):
            return None

        def dataframe(self, *args, **kwargs):
            return None

        def set_page_config(self, *args, **kwargs):
            return None

        def title(self, *args, **kwargs):
            return None

        def sidebar(self, *args, **kwargs):
            return self

        def radio(self, *args, **kwargs):
            return kwargs.get("value") if "value" in kwargs else None

        def text_input(self, *args, **kwargs):
            return kwargs.get("value", "")

        def tabs(self, *args, **kwargs):
            n = len(args[0]) if args else 1
            return [self for _ in range(n)]

        @property
        def session_state(self):
            if not hasattr(self, "_session_state"):
                self._session_state = {}
            return self._session_state

    st = _DummySt()

from analysis.config import PARTY_COLOURS

logger = logging.getLogger(__name__)


def _render_scree_plot(importances: List[float], n_show: int = 15) -> None:
    """Render a scree plot showing relative SVD component importance.

    Highlighted bars for the top-2 components (used in the compass); muted bars
    for the rest.  A cumulative-variance dashed line on the same y-axis helps
    spot the elbow.  A 50 % cumulative threshold line is drawn for reference.

    Args:
        importances: List of importance values sorted descending (from load_scree_data).
        n_show: How many components to display (default: first 15).
    """
    if not importances:
        return
    data = list(importances[:n_show])
    ranks = list(range(1, len(data) + 1))

    cumsum = []
    running = 0.0
    for v in data:
        running += v
        cumsum.append(running)

    n_highlight = 2
    bar_colours = [
        "#1565C0" if i < n_highlight else "#90CAF9" for i in range(len(data))
    ]

    fig = go.Figure()

    fig.add_trace(
        go.Bar(
            x=ranks,
            y=data,
            marker_color=bar_colours,
            hovertemplate="As %{x}<br><b>%{y:.1f}%</b> verklaarde variantie<extra></extra>",
            showlegend=False,
        )
    )

    fig.add_trace(
        go.Scatter(
            x=ranks,
            y=cumsum,
            mode="lines+markers",
            line={"color": "#F57C00", "width": 2, "dash": "dot"},
            marker={"size": 5, "color": "#F57C00"},
            hovertemplate="As %{x}<br>Cumulatief: <b>%{y:.1f}%</b><extra></extra>",
            name="Cumulatief",
            showlegend=True,
        )
    )

    fig.add_hline(
        y=50,
        line_dash="dash",
        line_color="#BDBDBD",
        line_width=1,
        annotation_text="50%",
        annotation_position="right",
        annotation_font_color="#9E9E9E",
        annotation_font_size=11,
    )

    for i in range(min(n_highlight, len(data))):
        fig.add_annotation(
            x=ranks[i],
            y=data[i] + 0.3,
            text=f"{data[i]:.1f}%",
            showarrow=False,
            font={"size": 11, "color": "#1565C0"},
            yanchor="bottom",
        )

    fig.update_layout(
        height=280,
        margin={"l": 10, "r": 50, "t": 30, "b": 40},
        title={
            "text": "Belang per SVD-as",
            "font": {"size": 13, "color": "#555555"},
            "x": 0.02,
            "xanchor": "left",
        },
        legend={
            "orientation": "h",
            "x": 0.5,
            "xanchor": "center",
            "y": 1.08,
            "font": {"size": 11},
        },
        xaxis={
            "title": {"text": "As (rang)", "font": {"size": 11}},
            "tickmode": "linear",
            "tick0": 1,
            "dtick": 1,
            "showline": False,
            "showgrid": False,
        },
        yaxis={
            "title": {"text": "% van totale variantie", "font": {"size": 11}},
            "showline": False,
            "showgrid": True,
            "gridcolor": "#eeeeee",
            "ticksuffix": "%",
            "range": [0, max(cumsum) * 1.08],
        },
        plot_bgcolor="rgba(0,0,0,0)",
        paper_bgcolor="rgba(0,0,0,0)",
        bargap=0.25,
    )
    st.plotly_chart(fig, use_container_width=True)


def _build_party_axis_figure(
    party_coords: Dict[str, Tuple[float, float]],
    comp_sel: int,
    theme: dict,
    bootstrap_data: Optional[Dict[str, Dict]] = None,
) -> Optional[go.Figure]:
    """Build a 1D horizontal Plotly scatter of party positions on SVD axis `comp_sel`.

    Accepts explicit per-party 2D coordinates (x,y) and uses the component selection to
    pick the value (comp_sel==1 -> x, comp_sel==2 -> y). This makes the API explicit and
    avoids indexing into long SVD vectors.

    Returns go.Figure or None if no data available.
    """
    if not party_coords:
        return None

    if comp_sel not in (1, 2):
        raise ValueError(
            "_build_party_axis_figure only supports comp_sel 1 or 2 when using explicit coords"
        )

    axis_idx = comp_sel - 1
    flip = theme.get("flip", False)

    parties = []
    scores = []
    colours = []

    for party, val in party_coords.items():
        try:
            if hasattr(val, "__len__") and len(val) == 2:
                x, y = val
                score = float(x if axis_idx == 0 else y)
            else:
                score = float(val[axis_idx])

            if flip:
                score = -score
        except Exception:
            continue

        parties.append(party)
        scores.append(score)
        colours.append(PARTY_COLOURS.get(party, "#9E9E9E"))

    if not scores:
        return None

    hover = []
    symbols = []
    if bootstrap_data:
        for p, s in zip(parties, scores):
            bd = bootstrap_data.get(p)
            if bd:
                n_mps = bd.get("n_mps", "?")
                ci_low = None
                ci_high = None
                try:
                    ci_low = float(bd["ci_lower"][axis_idx])
                    ci_high = float(bd["ci_upper"][axis_idx])
                except Exception:
                    pass
                if ci_low is not None and ci_high is not None:
                    hover.append(
                        f"{p}: {s:.3f} (N={n_mps}, 95%-BI: [{ci_low:.3f}, {ci_high:.3f}])"
                    )
                else:
                    hover.append(f"{p}: {s:.3f} (N={n_mps})")
                symbols.append("diamond" if n_mps == 1 else "circle")
            else:
                hover.append(f"{p}: {s:.3f}")
                symbols.append("circle")
        marker_kwargs = {"size": 14, "color": colours, "symbol": symbols}
    else:
        hover = [f"{p}: {s:.3f}" for p, s in zip(parties, scores)]
        marker_kwargs = {"size": 14, "color": colours}

    fig = go.Figure()
    x_min, x_max = min(scores) * 1.15, max(scores) * 1.15
    if x_min == x_max:
        x_min, x_max = x_min - 1, x_max + 1
    fig.add_trace(
        go.Scatter(
            x=[x_min, x_max],
            y=[0, 0],
            mode="lines",
            line={"color": "#cccccc", "width": 1},
            hoverinfo="skip",
            showlegend=False,
        )
    )

    scatter_kwargs = {
        "x": scores,
        "y": [0] * len(scores),
        "mode": "markers+text",
        "text": parties,
        "textposition": "top center",
        "marker": marker_kwargs,
        "hovertext": hover,
        "hoverinfo": "text",
        "showlegend": False,
    }
    fig.add_trace(go.Scatter(**scatter_kwargs))

    pos_pole = theme.get("positive_pole", "")
    neg_pole = theme.get("negative_pole", "")
    left_label = neg_pole
    right_label = pos_pole

    fig.update_layout(
        height=160,
        margin={"l": 10, "r": 10, "t": 10, "b": 30},
        xaxis={
            "title": f"← {left_label}  |  {right_label} →",
            "showticklabels": False,
            "showline": False,
            "showgrid": False,
            "zeroline": False,
        },
        yaxis={"visible": False, "range": [-1, 2]},
        plot_bgcolor="rgba(0,0,0,0)",
        paper_bgcolor="rgba(0,0,0,0)",
    )
    return fig


def _render_party_axis_chart(
    party_coords: Dict[str, Tuple[float, float]],
    comp_sel: int,
    theme: dict,
    bootstrap_data: Optional[Dict[str, Dict]] = None,
) -> None:
    """Render a 1D horizontal Plotly scatter of party positions on SVD axis `comp_sel`.

    Expects explicit per-party coords mapping (party -> (x,y)) for components 1 & 2.
    """
    fig = _build_party_axis_figure(party_coords, comp_sel, theme, bootstrap_data)
    if fig is None:
        st.caption("_Partijdata niet beschikbaar voor deze as._")
        return
    st.plotly_chart(fig, use_container_width=True)


def _render_party_axis_chart_1d(
    party_coords: Dict[str, Tuple[float, ...]],
    comp_sel: int,
    theme: dict,
) -> None:
    """Render a 1D horizontal scatter of party positions on SVD component `comp_sel`.

    Uses the same format as components 1-2: parties as markers on a horizontal line
    with axis title showing poles with arrows.

    Args:
        party_coords: Dict mapping party name to tuple of scores (score_for_comp,)
        comp_sel: SVD component number (1-indexed)
        theme: Dict with label, positive_pole, negative_pole, flip
    """
    if not party_coords:
        st.caption("_Partijdata niet beschikbaar voor deze as._")
        return

    parties = []
    scores = []
    colours = []

    for party, coords in party_coords.items():
        try:
            score = float(coords[0])
            parties.append(party)
            scores.append(score)
            colours.append(PARTY_COLOURS.get(party, "#9E9E9E"))
        except Exception:
            continue

    if not scores:
        st.caption("_Partijdata niet beschikbaar voor deze as._")
        return

    flip = theme.get("flip", False)
    if flip:
        scores = [-s for s in scores]

    hover = [f"{p}: {s:.3f}" for p, s in zip(parties, scores)]

    fig = go.Figure()
    x_min, x_max = min(scores) * 1.15, max(scores) * 1.15
    if x_min == x_max:
        x_min, x_max = x_min - 1, x_max + 1

    fig.add_trace(
        go.Scatter(
            x=[x_min, x_max],
            y=[0, 0],
            mode="lines",
            line={"color": "#cccccc", "width": 1},
            hoverinfo="skip",
            showlegend=False,
        )
    )

    fig.add_trace(
        go.Scatter(
            x=scores,
            y=[0] * len(scores),
            mode="markers+text",
            text=parties,
            textposition="top center",
            marker={"size": 14, "color": colours},
            hovertext=hover,
            hoverinfo="text",
            showlegend=False,
        )
    )

    pos_pole = theme.get("positive_pole", "")
    neg_pole = theme.get("negative_pole", "")
    left_label = neg_pole
    right_label = pos_pole

    fig.update_layout(
        height=160,
        margin={"l": 10, "r": 10, "t": 10, "b": 30},
        xaxis={
            "title": f"← {left_label}  |  {right_label} →",
            "showticklabels": False,
            "showline": False,
            "showgrid": False,
            "zeroline": False,
        },
        yaxis={"visible": False, "range": [-1, 2]},
        plot_bgcolor="rgba(0,0,0,0)",
        paper_bgcolor="rgba(0,0,0,0)",
    )

    st.plotly_chart(fig, use_container_width=True)


def _render_svd_time_trajectory(
    party_scores_by_window: Dict[str, Dict[str, List[float]]],
    comp_sel: int,
    theme: dict,
    selected_parties: List[str],
) -> None:
    """Render a time trajectory plot showing party positions over time on an SVD component.

    Args:
        party_scores_by_window: {window_id: {party_name: [scores]}}
        comp_sel: SVD component number (1-indexed)
        theme: Theme dict with label, positive_pole, negative_pole, flip
        selected_parties: List of party names to display
    """
    if not party_scores_by_window or not selected_parties:
        st.caption("_Geen data beschikbaar voor tijdtraject._")
        return

    idx = comp_sel - 1
    flip = theme.get("flip", False)

    party_trajectories: Dict[str, List[Tuple[str, float]]] = {}

    all_windows = list(party_scores_by_window.keys())
    sorted_windows = []
    if "current_parliament" in all_windows:
        sorted_windows.append("current_parliament")
    other_windows = sorted(
        [w for w in all_windows if w != "current_parliament"], reverse=True
    )
    sorted_windows.extend(other_windows)

    for window in sorted_windows:
        scores_by_party = party_scores_by_window.get(window, {})
        for party in selected_parties:
            scores = scores_by_party.get(party, [])
            if scores and len(scores) > idx:
                try:
                    score = float(scores[idx])
                    if flip:
                        score = -score
                    party_trajectories.setdefault(party, []).append((window, score))
                except (ValueError, TypeError):
                    continue

    if not party_trajectories:
        st.caption("_Geen data beschikbaar voor geselecteerde partijen._")
        return

    fig = go.Figure()

    all_scores = []
    for traj in party_trajectories.values():
        all_scores.extend([s for _, s in traj])

    if not all_scores:
        st.caption("_Geen scores beschikbaar._")
        return

    x_min, x_max = min(all_scores) * 1.15, max(all_scores) * 1.15
    if x_min == x_max:
        x_min, x_max = x_min - 1, x_max + 1

    window_to_y = {w: i for i, w in enumerate(sorted_windows)}

    for window in sorted_windows:
        y_pos = window_to_y[window]
        fig.add_trace(
            go.Scatter(
                x=[x_min, x_max],
                y=[y_pos, y_pos],
                mode="lines",
                line={"color": "#cccccc", "width": 1},
                hoverinfo="skip",
                showlegend=False,
            )
        )

    for party in selected_parties:
        if party not in party_trajectories:
            continue

        traj = party_trajectories[party]
        if len(traj) < 1:
            continue

        x_vals = [score for _, score in traj]
        y_vals = [window_to_y[window] for window, _ in traj]
        color = PARTY_COLOURS.get(party, "#9E9E9E")

        fig.add_trace(
            go.Scatter(
                x=x_vals,
                y=y_vals,
                mode="lines",
                line={"color": color, "width": 2},
                hoverinfo="skip",
                showlegend=False,
            )
        )

        hover_texts = [f"{party}<br>{window}: {score:.3f}" for window, score in traj]
        fig.add_trace(
            go.Scatter(
                x=x_vals,
                y=y_vals,
                mode="markers+text",
                text=[party] * len(traj),
                textposition="top center",
                marker={"size": 12, "color": color},
                hovertext=hover_texts,
                hoverinfo="text",
                showlegend=False,
            )
        )

    pos_pole = theme.get("positive_pole", "")
    neg_pole = theme.get("negative_pole", "")
    left_label = neg_pole
    right_label = pos_pole

    y_labels = {}
    for window in sorted_windows:
        if window == "current_parliament":
            y_labels[window_to_y[window]] = "Huidig"
        else:
            y_labels[window_to_y[window]] = window

    fig.update_layout(
        height=max(400, len(sorted_windows) * 60 + 100),
        margin={"l": 80, "r": 10, "t": 10, "b": 30},
        xaxis={
            "title": f"← {left_label}  |  {right_label} →",
            "range": [x_min, x_max],
            "showticklabels": False,
            "showline": False,
            "showgrid": True,
            "gridcolor": "rgba(0,0,0,0.1)",
            "zeroline": True,
            "zerolinecolor": "rgba(0,0,0,0.2)",
        },
        yaxis={
            "tickvals": list(y_labels.keys()),
            "ticktext": list(y_labels.values()),
            "tickmode": "array",
            "autorange": "reversed",
            "showgrid": False,
        },
        plot_bgcolor="rgba(0,0,0,0)",
        paper_bgcolor="rgba(0,0,0,0)",
    )

    st.plotly_chart(fig, use_container_width=True)


def _render_voting_results(voting_results_json) -> None:
    """Render a voting_results JSON blob as a grouped voor/tegen/onthouden table.

    The JSON is stored as {party_or_mp: vote} where vote is one of
    'voor', 'tegen', 'onthouden', 'afwezig'. We group by vote for readability.
    """
    if not voting_results_json:
        return
    try:
        vdata = (
            json.loads(voting_results_json)
            if isinstance(voting_results_json, str)
            else voting_results_json
        )
        if not isinstance(vdata, dict) or not vdata:
            return
        by_vote: Dict[str, List[str]] = {}
        for actor, vote in vdata.items():
            vote_str = str(vote).lower().strip()
            by_vote.setdefault(vote_str, []).append(str(actor))
        vote_order = ["voor", "tegen", "onthouden", "afwezig"]
        rows_shown = False
        for v in vote_order + [k for k in by_vote if k not in vote_order]:
            actors = by_vote.get(v)
            if not actors:
                continue
            st.markdown(
                f"**{v.capitalize()}** ({len(actors)}): {', '.join(sorted(actors))}"
            )
            rows_shown = True
        if not rows_shown:
            st.caption("_Geen stemuitslag beschikbaar_")
    except Exception:
        pass


def _add_y_direction_annotations(fig: go.Figure) -> None:
    """Add Progressief / Conservatief labels above and below the Y axis."""
    common = dict(
        xref="paper",
        yref="paper",
        x=-0.07,
        showarrow=False,
        font=dict(size=11, color="#666666"),
    )
    fig.add_annotation(**common, y=1.02, text="Progressief", xanchor="center")
    fig.add_annotation(**common, y=-0.06, text="Conservatief", xanchor="center")
