"""Plotly-based plot engine.

Renders PlotSpec objects into interactive Plotly figures with
support for corridors, dual X-cursors, and export.

The PlotEngine is the **single rendering path** — both the scripting
API and the web UI construct PlotSpec objects and delegate here.
"""

from __future__ import annotations

from typing import Any

import numpy as np
import plotly.graph_objects as go

from impakt.channel.model import Channel
from impakt.plot.spec import ChannelRef, Corridor, CursorValues, PlotSpec, PlotStyle

# Default color palette (colorblind-friendly)
DEFAULT_COLORS = [
    "#1f77b4",  # blue
    "#ff7f0e",  # orange
    "#2ca02c",  # green
    "#d62728",  # red
    "#9467bd",  # purple
    "#8c564b",  # brown
    "#e377c2",  # pink
    "#7f7f7f",  # gray
    "#bcbd22",  # olive
    "#17becf",  # cyan
]


class PlotEngine:
    """Renders PlotSpec into Plotly figures.

    Supports two modes:
    - **Default**: Full interactive plot with hover tooltips and legend.
    - **Compact** (``spec.compact=True``): Web UI mode with no legend, no
      hover tooltips, tight margins, smaller axis labels. Cursor tracking
      is handled by external JS.
    """

    def render(self, spec: PlotSpec) -> go.Figure:
        """Render a PlotSpec into an interactive Plotly figure."""
        fig = go.Figure()

        if not spec.channels:
            fig.update_layout(
                template="plotly_white",
                annotations=[
                    {
                        "text": "Select channels to plot",
                        "xref": "paper",
                        "yref": "paper",
                        "x": 0.5,
                        "y": 0.5,
                        "showarrow": False,
                        "font": {"size": 14, "color": "#bbb"},
                    }
                ],
                xaxis={"visible": False},
                yaxis={"visible": False},
                margin={"l": 40, "r": 20, "t": 30, "b": 40},
            )
            return fig

        # Add corridor fills first (behind data traces)
        for corridor in spec.corridors:
            self._add_corridor(fig, corridor)

        # Add channel traces
        for i, ch_ref in enumerate(spec.channels):
            ch = ch_ref.channel
            if ch is None:
                continue

            # Apply transforms if any
            if ch_ref.transform_chain:
                ch = ch_ref.transform_chain.apply(ch)

            style = ch_ref.style
            color = style.color or DEFAULT_COLORS[i % len(DEFAULT_COLORS)]
            label = style.label or ch_ref.label

            trace_kwargs: dict[str, Any] = {
                "x": ch.time,
                "y": ch.data,
                "mode": "lines",
                "name": label,
                "line": dict(
                    color=color,
                    width=style.line_width,
                    dash=style.line_dash,
                ),
                "opacity": style.opacity,
            }

            if not spec.compact:
                trace_kwargs["hovertemplate"] = (
                    f"{label}<br>t=%{{x:.6f}}s<br>%{{y:.4f}} {ch.unit}<extra></extra>"
                )

            fig.add_trace(go.Scatter(**trace_kwargs))

        # Add cursor lines
        if spec.x_cursors:
            x1, x2 = spec.x_cursors
            if spec.compact:
                # Color-coded cursor lines for compact mode
                fig.add_vline(
                    x=x1,
                    line_dash="dash",
                    line_color="rgba(220,53,69,0.6)",
                    line_width=1,
                    annotation_text=f"X1={x1:.4f}s",
                    annotation_font_size=9,
                    annotation_font_color="rgba(220,53,69,0.8)",
                )
                fig.add_vline(
                    x=x2,
                    line_dash="dash",
                    line_color="rgba(13,110,253,0.6)",
                    line_width=1,
                    annotation_text=f"X2={x2:.4f}s",
                    annotation_font_size=9,
                    annotation_font_color="rgba(13,110,253,0.8)",
                )
            else:
                for x_val, lbl in [(x1, "x1"), (x2, "x2")]:
                    fig.add_vline(
                        x=x_val,
                        line_dash="dash",
                        line_color="gray",
                        line_width=1,
                        annotation_text=f"{lbl}={x_val:.6f}s",
                        annotation_position="top",
                    )

        # Layout
        if spec.compact:
            margin = spec.margin or {"l": 45, "r": 8, "t": 4, "b": 28}
            fig.update_layout(
                xaxis_title=dict(text=spec.x_label, font=dict(size=10, color="#999")),
                yaxis_title=dict(text=spec.y_label, font=dict(size=10, color="#999")),
                template="plotly_white",
                hovermode=False,
                showlegend=False,
                margin=margin,
            )
        else:
            margin = spec.margin or {"l": 60, "r": 20, "t": 40 if spec.title else 10, "b": 60}
            hovermode = spec.hovermode if spec.hovermode is not None else "x unified"
            fig.update_layout(
                title=spec.title,
                xaxis_title=spec.x_label,
                yaxis_title=spec.y_label,
                showlegend=spec.show_legend,
                height=spec.height,
                width=spec.width,
                template="plotly_white",
                hovermode=hovermode,
                legend=dict(
                    orientation="h",
                    yanchor="bottom",
                    y=-0.3,
                    xanchor="center",
                    x=0.5,
                ),
                margin=margin,
            )

        if spec.show_grid:
            fig.update_xaxes(showgrid=True, gridwidth=1, gridcolor="rgba(128,128,128,0.2)")
            fig.update_yaxes(showgrid=True, gridwidth=1, gridcolor="rgba(128,128,128,0.2)")

        if spec.x_range:
            fig.update_xaxes(range=list(spec.x_range))
        if spec.y_range:
            fig.update_yaxes(range=list(spec.y_range))

        return fig

    def _add_corridor(self, fig: go.Figure, corridor: Corridor) -> None:
        """Add a corridor (tolerance band) to the figure."""
        style = corridor.style

        fig.add_trace(
            go.Scatter(
                x=corridor.time,
                y=corridor.upper,
                mode="lines",
                name=f"{corridor.name} (upper)",
                line=dict(color=style.line_color, width=style.line_width, dash=style.line_dash),
                showlegend=False,
            )
        )
        fig.add_trace(
            go.Scatter(
                x=corridor.time,
                y=corridor.lower,
                mode="lines",
                name=f"{corridor.name} (lower)",
                line=dict(color=style.line_color, width=style.line_width, dash=style.line_dash),
                fill="tonexty",
                fillcolor=style.fill_color,
                showlegend=True,
            )
        )

    def to_image(self, spec: PlotSpec, format: str = "png", scale: float = 2.0) -> bytes:
        """Render to a static image."""
        fig = self.render(spec)
        return fig.to_image(format=format, scale=scale)

    def to_html(self, spec: PlotSpec, include_plotlyjs: bool = True) -> str:
        """Render to standalone HTML."""
        fig = self.render(spec)
        return fig.to_html(include_plotlyjs=include_plotlyjs)


def cursor_values(
    spec_or_channels: PlotSpec | list[Channel],
    x1: float,
    x2: float,
) -> CursorValues:
    """Compute interpolated values at two X-axis positions."""
    channels: list[tuple[str, Channel]] = []

    if isinstance(spec_or_channels, PlotSpec):
        for ref in spec_or_channels.channels:
            ch = ref.channel
            if ch is None:
                continue
            if ref.transform_chain:
                ch = ref.transform_chain.apply(ch)
            channels.append((ref.label, ch))
    else:
        for ch in spec_or_channels:
            channels.append((ch.code.short_label if ch.code.is_valid else ch.name, ch))

    values = []
    for label, ch in channels:
        v1 = ch.value_at(x1)
        v2 = ch.value_at(x2)
        values.append(
            {
                "label": label,
                "value_at_x1": v1,
                "value_at_x2": v2,
                "delta": v2 - v1,
                "unit": ch.unit,
            }
        )

    return CursorValues(x1=x1, x2=x2, values=values)
