"""Plot rendering callbacks.

Handles:
- Updating plot figures when channels/transforms change
- Cursor line rendering (X1/X2 vertical lines)
- Hover data is NOT shown as a Plotly tooltip — instead the cursor grid
  picks it up via the hoverData callback
"""

from __future__ import annotations

from collections import defaultdict
from typing import Any

import dash
import numpy as np
import plotly.graph_objects as go
from dash import ALL, Input, Output, State, ctx
from dash.exceptions import PreventUpdate

from impakt.channel.model import Channel
from impakt.plot.engine import DEFAULT_COLORS
from impakt.transform.align import XAlign, YAlign
from impakt.transform.cfc import CFCFilter
from impakt.transform.resultant import resultant_from_channels
from impakt.web.state import AppState


def _resolve_channels(
    selected_keys: list[str],
    app_state: AppState,
    cfc_value: str,
    y_align: bool,
    x_align_method: str,
    x_align_value: float | None,
    show_resultant: bool,
) -> list[tuple[str, Channel]]:
    """Resolve selected channel keys to Channel objects with transforms applied.

    Per-channel overrides (from app_state.channel_overrides) take precedence
    over the global CFC setting.
    """
    channels: list[tuple[str, Channel]] = []

    for key in selected_keys:
        if "::" in key:
            test_id, ch_name = key.split("::", 1)
        elif app_state.primary_test:
            test_id = app_state.primary_test.test_id
            ch_name = key
        else:
            continue

        ch = app_state.get_channel(test_id, ch_name)
        if ch is None:
            continue

        # Determine CFC: per-channel override takes precedence over global
        override = app_state.channel_overrides.get(key, {})
        ch_cfc = override.get("cfc", "")
        effective_cfc = ch_cfc if ch_cfc else cfc_value

        if effective_cfc and effective_cfc != "none":
            try:
                ch = CFCFilter(cfc_class=int(effective_cfc)).apply(ch)
            except (ValueError, Exception):
                pass

        # Apply Y-align
        if y_align:
            ch = YAlign().apply(ch)

        # Apply X-align
        if x_align_method == "manual" and x_align_value is not None:
            ch = XAlign(method="manual", reference_time=x_align_value).apply(ch)
        elif x_align_method == "threshold" and x_align_value is not None:
            ch = XAlign(method="threshold", threshold_value=x_align_value).apply(ch)

        # Build label
        multi_test = len(app_state.tests) > 1
        label = ch.code.short_label if ch.code.is_valid else ch.name
        if multi_test:
            label = f"[{test_id}] {label}"

        channels.append((label, ch))

    # Compute resultants if requested
    if show_resultant and len(channels) >= 2:
        groups: dict[str, list[tuple[str, Channel]]] = defaultdict(list)
        for label, ch in channels:
            if ch.code.is_valid and ch.code.is_component():
                groups[ch.code.group_key()].append((label, ch))

        for gkey, group_channels in groups.items():
            if len(group_channels) >= 2:
                try:
                    comps = [ch for _, ch in group_channels]
                    res = resultant_from_channels(*comps)
                    res_label = (
                        f"{res.code.location_label} Resultant" if res.code.is_valid else "Resultant"
                    )
                    if len(app_state.tests) > 1:
                        res_label = f"[{comps[0].source_test_id}] {res_label}"
                    channels.append((res_label, res))
                except Exception:
                    pass

    return channels


def _build_figure(
    channels: list[tuple[str, Channel]],
    cursor_x1: float | None,
    cursor_x2: float | None,
    cfc_value: str,
    corridors: list[dict] | None = None,
) -> go.Figure:
    """Build a Plotly figure from resolved channels."""
    fig = go.Figure()

    if not channels:
        fig.update_layout(
            template="plotly_white",
            annotations=[
                {
                    "text": "Select channels from the grid 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 traces — hovermode is disabled at the layout level; cursor tracking
    # is handled by our own JS mousemove handler (cursor_tracker.js).
    for i, (label, ch) in enumerate(channels):
        color = DEFAULT_COLORS[i % len(DEFAULT_COLORS)]
        fig.add_trace(
            go.Scatter(
                x=ch.time.tolist(),
                y=ch.data.tolist(),
                mode="lines",
                name=label,
                line=dict(color=color, width=1.5),
            )
        )

    # Add corridor fills
    if corridors:
        for corridor in corridors:
            if not corridor.get("visible", True):
                continue
            c_time = corridor["time"]
            c_upper = corridor["upper"]
            c_lower = corridor["lower"]
            c_name = corridor.get("name", "Corridor")

            # Upper bound
            fig.add_trace(
                go.Scatter(
                    x=c_time,
                    y=c_upper,
                    mode="lines",
                    line=dict(color="rgba(100,100,255,0.4)", width=1, dash="dash"),
                    showlegend=False,
                    name=f"{c_name} upper",
                )
            )
            # Lower bound with fill to upper
            fig.add_trace(
                go.Scatter(
                    x=c_time,
                    y=c_lower,
                    mode="lines",
                    line=dict(color="rgba(100,100,255,0.4)", width=1, dash="dash"),
                    fill="tonexty",
                    fillcolor="rgba(100,100,255,0.1)",
                    showlegend=True,
                    name=c_name,
                )
            )

    # Add X1/X2 cursor lines
    if cursor_x1 is not None:
        fig.add_vline(
            x=cursor_x1,
            line_dash="dash",
            line_color="rgba(220,53,69,0.6)",
            line_width=1,
            annotation_text=f"X1={cursor_x1:.4f}s",
            annotation_font_size=9,
            annotation_font_color="rgba(220,53,69,0.8)",
        )
    if cursor_x2 is not None:
        fig.add_vline(
            x=cursor_x2,
            line_dash="dash",
            line_color="rgba(13,110,253,0.6)",
            line_width=1,
            annotation_text=f"X2={cursor_x2:.4f}s",
            annotation_font_size=9,
            annotation_font_color="rgba(13,110,253,0.8)",
        )

    # Layout — hovermode is disabled; cursor tracking is handled entirely
    # by our JS (cursor_tracker.js) which reads pixel positions from mousemove
    # events and converts to data coordinates via Plotly's axis internals.
    y_label = channels[0][1].unit if channels else ""

    fig.update_layout(
        xaxis_title=dict(text="Time (s)", font=dict(size=10, color="#999")),
        yaxis_title=dict(text=y_label, font=dict(size=10, color="#999")),
        template="plotly_white",
        hovermode=False,
        showlegend=False,
        margin=dict(l=45, r=8, t=4, b=28),
    )

    return fig


def register_plot_callbacks(app: dash.Dash, app_state: AppState) -> None:
    """Register all plot-related callbacks."""

    @app.callback(
        Output({"type": "plot-graph", "index": 0}, "figure"),
        [
            Input("selected-channels-store", "data"),
            Input("cfc-select", "value"),
            Input("show-resultant", "value"),
            Input("y-align-check", "value"),
            Input("x-align-method", "value"),
            Input("cursor-x1", "value"),
            Input("cursor-x2", "value"),
            Input("channel-overrides-store", "data"),
            Input("corridors-store", "data"),
        ],
        [
            State("x-align-value", "value"),
        ],
    )
    def update_main_plot(
        selected_keys: list[str] | None,
        cfc_value: str,
        show_resultant: bool,
        y_align: bool,
        x_align_method: str,
        cursor_x1: float | None,
        cursor_x2: float | None,
        channel_overrides: dict | None,
        corridors_data: list[dict] | None,
        x_align_value: float | None,
    ) -> go.Figure:
        if not selected_keys:
            selected_keys = []

        channels = _resolve_channels(
            selected_keys,
            app_state,
            cfc_value,
            y_align,
            x_align_method or "none",
            x_align_value,
            show_resultant,
        )

        return _build_figure(
            channels,
            cursor_x1,
            cursor_x2,
            cfc_value,
            corridors=corridors_data,
        )
