"""Channel selection and filtering callbacks.

Handles:
- DataTable row selection -> updates selected channels store
- Filter/facet changes -> filters table rows AND recomputes selected_rows
  indices to preserve selection across filter changes
- Selected channels badge display with consistent colors
- Per-channel CFC override controls
"""

from __future__ import annotations

from typing import Any

import dash
from dash import ALL, Input, Output, State, html, no_update
from dash.exceptions import PreventUpdate

from impakt.plot.engine import DEFAULT_COLORS
from impakt.web.components.channel_grid import (
    build_selected_channels_badges,
    filter_rows,
)
from impakt.web.components.transforms import build_per_channel_override_rows
from impakt.web.state import AppState


def register_channel_callbacks(app: dash.Dash, app_state: AppState) -> None:
    """Register all channel selection and filtering callbacks."""

    @app.callback(
        Output("selected-channels-store", "data"),
        [Input("channel-grid", "selected_rows")],
        [
            State("channel-grid", "data"),
            State("selected-channels-store", "data"),
        ],
    )
    def sync_selection_to_store(
        selected_row_indices: list[int] | None,
        visible_data: list[dict[str, Any]] | None,
        prev_selected: list[str] | None,
    ) -> list[str]:
        """When user checks/unchecks rows, merge with existing selection.

        The key insight: selected_rows are indices into the *currently visible*
        data (after filtering). We need to:
        1. Determine which visible rows are now checked
        2. Keep any previously-selected rows that aren't visible (filtered out)
        3. Remove any visible rows that were unchecked
        """
        if visible_data is None:
            return prev_selected or []

        prev = set(prev_selected or [])
        visible_keys = {row["key"] for row in visible_data}

        # Keys currently checked in the visible table
        checked_keys: set[str] = set()
        if selected_row_indices:
            for idx in selected_row_indices:
                if 0 <= idx < len(visible_data):
                    checked_keys.add(visible_data[idx]["key"])

        # Start with previously selected keys that are NOT visible
        # (i.e., they were selected before a filter was applied — preserve them)
        result = [k for k in (prev_selected or []) if k not in visible_keys]

        # Add the checked visible keys (in visible order)
        for row in visible_data:
            if row["key"] in checked_keys:
                result.append(row["key"])

        return result

    @app.callback(
        Output("selected-channels-badges", "children"),
        [Input("selected-channels-store", "data")],
    )
    def update_badges(selected_keys: list[str] | None) -> list:
        return build_selected_channels_badges(selected_keys or [], app_state)

    @app.callback(
        [
            Output("channel-grid", "data"),
            Output("channel-grid", "selected_rows"),
            Output("channel-grid", "style_data_conditional"),
        ],
        [
            Input("channel-filter-input", "value"),
            Input("channel-filter-clear", "n_clicks"),
            Input("facet-body", "value"),
            Input("facet-meas", "value"),
            Input("facet-direction", "value"),
            Input("selected-channels-store", "data"),
        ],
        [State("channel-grid-all-rows", "data")],
    )
    def apply_filters_and_selection(
        pattern: str | None,
        clear_clicks: int | None,
        body: str | None,
        meas: str | None,
        direction: str | None,
        selected_keys: list[str] | None,
        all_rows: list[dict[str, Any]] | None,
    ) -> tuple[list[dict[str, Any]], list[int], list[dict]]:
        """Filter rows AND recompute selected_rows + color styling."""
        if not all_rows:
            return [], [], []

        trigger = dash.ctx.triggered_id
        if trigger == "channel-filter-clear":
            filtered = all_rows
        else:
            filtered = filter_rows(
                all_rows,
                pattern=pattern or "",
                body=body or "",
                meas=meas or "",
                direction=direction or "",
            )

        # Build a lookup of selected keys to their color index
        selected_set = set(selected_keys or [])
        selected_list = list(selected_keys or [])
        color_map: dict[str, int] = {}
        for i, key in enumerate(selected_list):
            color_map[key] = i

        # Compute selected_rows indices in the filtered data
        selected_indices = []
        for idx, row in enumerate(filtered):
            if row["key"] in selected_set:
                selected_indices.append(idx)

        # Build style_data_conditional for coloring selected rows
        style_cond: list[dict] = []
        for idx, row in enumerate(filtered):
            if row["key"] in color_map:
                ci = color_map[row["key"]]
                color = DEFAULT_COLORS[ci % len(DEFAULT_COLORS)]
                style_cond.append(
                    {
                        "if": {"row_index": idx},
                        "backgroundColor": f"{color}18",  # Very light tint
                        "borderLeft": f"3px solid {color}",
                    }
                )

        return filtered, selected_indices, style_cond

    @app.callback(
        [
            Output("channel-filter-input", "value"),
            Output("facet-body", "value"),
            Output("facet-meas", "value"),
            Output("facet-direction", "value"),
        ],
        [Input("channel-filter-clear", "n_clicks")],
        prevent_initial_call=True,
    )
    def clear_filters(n_clicks: int | None) -> tuple[str, str, str, str]:
        """Clear all filter inputs."""
        return "", "", "", ""

    @app.callback(
        Output("per-channel-overrides", "children"),
        [Input("selected-channels-store", "data")],
    )
    def update_per_channel_overrides(selected_keys: list[str] | None) -> list:
        """Show per-channel CFC override controls for selected channels."""
        return build_per_channel_override_rows(
            selected_keys or [],
            app_state.channel_overrides,
        )

    @app.callback(
        Output("channel-overrides-store", "data"),
        [Input({"type": "ch-cfc-override", "index": ALL}, "value")],
        [State({"type": "ch-cfc-override", "index": ALL}, "id")],
        prevent_initial_call=True,
    )
    def sync_per_channel_overrides(
        values: list[str],
        ids: list[dict[str, str]],
    ) -> dict[str, dict[str, str]]:
        """Sync per-channel CFC overrides to app state."""
        for item_id, value in zip(ids, values):
            key = item_id["index"]
            if value:
                app_state.channel_overrides[key] = {"cfc": value}
            else:
                app_state.channel_overrides.pop(key, None)
        return dict(app_state.channel_overrides)
