"""
Pipeline UI panel - Interactive controls for pipeline configuration.

Provides:
- Stage list with enable/disable toggles
- Parameter sliders for selected effect
- Keyboard/mouse interaction

This module implements the right-side UI panel that appears in border="ui" mode.
"""

from collections.abc import Callable
from dataclasses import dataclass, field
from typing import Any


@dataclass
class UIConfig:
    """Configuration for the UI panel."""

    panel_width: int = 24  # Characters wide
    stage_list_height: int = 12  # Number of stages to show at once
    param_height: int = 8  # Space for parameter controls
    scroll_offset: int = 0  # Scroll position in stage list
    start_with_preset_picker: bool = False  # Show preset picker immediately


@dataclass
class StageControl:
    """Represents a stage in the UI panel with its toggle state."""

    name: str
    stage_name: str  # Actual pipeline stage name
    category: str
    enabled: bool = True
    selected: bool = False
    params: dict[str, Any] = field(default_factory=dict)  # Current param values
    param_schema: dict[str, dict] = field(default_factory=dict)  # Param metadata

    def toggle(self) -> None:
        """Toggle enabled state."""
        self.enabled = not self.enabled

    def get_param(self, name: str) -> Any:
        """Get current parameter value."""
        return self.params.get(name)

    def set_param(self, name: str, value: Any) -> None:
        """Set parameter value."""
        self.params[name] = value


class UIPanel:
    """Interactive UI panel for pipeline configuration.

    Manages:
    - Stage list with enable/disable checkboxes
    - Parameter sliders for selected stage
    - Keyboard/mouse event handling
    - Scroll state for long stage lists

    The panel is rendered as a right border (panel_width characters wide)
    alongside the main viewport.
    """

    def __init__(self, config: UIConfig | None = None):
        self.config = config or UIConfig()
        self.stages: dict[str, StageControl] = {}  # stage_name -> StageControl
        self.scroll_offset = 0
        self.selected_stage: str | None = None
        self._focused_param: str | None = None  # For slider adjustment
        self._callbacks: dict[str, Callable] = {}  # Event callbacks
        self._presets: list[str] = []  # Available preset names
        self._current_preset: str = ""  # Current preset name
        self._show_preset_picker: bool = (
            config.start_with_preset_picker if config else False
        )  # Picker overlay visible
        self._show_panel: bool = True  # UI panel visibility
        self._preset_scroll_offset: int = 0  # Scroll in preset list

    def save_state(self) -> dict[str, Any]:
        """Save UI panel state for restoration after pipeline rebuild.

        Returns:
            Dictionary containing UI panel state that can be restored
        """
        # Save stage control states (enabled, params, etc.)
        stage_states = {}
        for name, ctrl in self.stages.items():
            stage_states[name] = {
                "enabled": ctrl.enabled,
                "selected": ctrl.selected,
                "params": dict(ctrl.params),  # Copy params dict
            }

        return {
            "stage_states": stage_states,
            "scroll_offset": self.scroll_offset,
            "selected_stage": self.selected_stage,
            "_focused_param": self._focused_param,
            "_show_panel": self._show_panel,
            "_show_preset_picker": self._show_preset_picker,
            "_preset_scroll_offset": self._preset_scroll_offset,
        }

    def restore_state(self, state: dict[str, Any]) -> None:
        """Restore UI panel state from saved state.

        Args:
            state: Dictionary containing UI panel state from save_state()
        """
        # Restore stage control states
        stage_states = state.get("stage_states", {})
        for name, stage_state in stage_states.items():
            if name in self.stages:
                ctrl = self.stages[name]
                ctrl.enabled = stage_state.get("enabled", True)
                ctrl.selected = stage_state.get("selected", False)
                # Restore params
                saved_params = stage_state.get("params", {})
                for param_name, param_value in saved_params.items():
                    if param_name in ctrl.params:
                        ctrl.params[param_name] = param_value

        # Restore UI panel state
        self.scroll_offset = state.get("scroll_offset", 0)
        self.selected_stage = state.get("selected_stage")
        self._focused_param = state.get("_focused_param")
        self._show_panel = state.get("_show_panel", True)
        self._show_preset_picker = state.get("_show_preset_picker", False)
        self._preset_scroll_offset = state.get("_preset_scroll_offset", 0)

    def register_stage(self, stage: Any, enabled: bool = True) -> StageControl:
        """Register a stage for UI control.

        Args:
            stage: Stage instance (must have .name, .category attributes)
            enabled: Initial enabled state

        Returns:
            The created StageControl instance
        """
        control = StageControl(
            name=stage.name,
            stage_name=stage.name,
            category=stage.category,
            enabled=enabled,
        )
        self.stages[stage.name] = control
        return control

    def unregister_stage(self, stage_name: str) -> None:
        """Remove a stage from UI control."""
        if stage_name in self.stages:
            del self.stages[stage_name]

    def get_enabled_stages(self) -> list[str]:
        """Get list of stage names that are currently enabled."""
        return [name for name, ctrl in self.stages.items() if ctrl.enabled]

    def select_stage(self, stage_name: str | None = None) -> None:
        """Select a stage (for parameter editing)."""
        if stage_name in self.stages:
            self.selected_stage = stage_name
            self.stages[stage_name].selected = True
            # Deselect others
            for name, ctrl in self.stages.items():
                if name != stage_name:
                    ctrl.selected = False
            # Auto-focus first parameter when stage selected
            if self.stages[stage_name].params:
                self._focused_param = next(iter(self.stages[stage_name].params.keys()))
            else:
                self._focused_param = None

    def toggle_stage(self, stage_name: str) -> bool:
        """Toggle a stage's enabled state.

        Returns:
            New enabled state
        """
        if stage_name in self.stages:
            ctrl = self.stages[stage_name]
            ctrl.enabled = not ctrl.enabled
            return ctrl.enabled
        return False

    def adjust_selected_param(self, delta: float) -> None:
        """Adjust the currently focused parameter of selected stage.

        Args:
            delta: Amount to add (positive or negative)
        """
        if self.selected_stage and self._focused_param:
            ctrl = self.stages[self.selected_stage]
            if self._focused_param in ctrl.params:
                current = ctrl.params[self._focused_param]
                # Determine step size from schema
                schema = ctrl.param_schema.get(self._focused_param, {})
                step = schema.get("step", 0.1 if isinstance(current, float) else 1)
                new_val = current + delta * step
                # Clamp to min/max if specified
                if "min" in schema:
                    new_val = max(schema["min"], new_val)
                if "max" in schema:
                    new_val = min(schema["max"], new_val)
                # Only emit if value actually changed
                if new_val != current:
                    ctrl.params[self._focused_param] = new_val
                    self._emit_event(
                        "param_changed",
                        stage_name=self.selected_stage,
                        param_name=self._focused_param,
                        value=new_val,
                    )

    def scroll_stages(self, delta: int) -> None:
        """Scroll the stage list."""
        max_offset = max(0, len(self.stages) - self.config.stage_list_height)
        self.scroll_offset = max(0, min(max_offset, self.scroll_offset + delta))

    def render(self, width: int, height: int) -> list[str]:
        """Render the UI panel.

        Args:
            width: Total display width (panel uses last `panel_width` cols)
            height: Total display height

        Returns:
            List of strings, each of length `panel_width`, to overlay on right side
        """
        panel_width = min(
            self.config.panel_width, width - 4
        )  # Reserve at least 2 for main
        lines = []

        # If panel is hidden, render empty space
        if not self._show_panel:
            return [" " * panel_width for _ in range(height)]

        # If preset picker is active, render that overlay instead of normal panel
        if self._show_preset_picker:
            picker_lines = self._render_preset_picker(panel_width)
            # Pad to full panel height if needed
            while len(picker_lines) < height:
                picker_lines.append(" " * panel_width)
            return [
                line.ljust(panel_width)[:panel_width] for line in picker_lines[:height]
            ]

        # Header
        title_line = "┌" + "─" * (panel_width - 2) + "┐"
        lines.append(title_line)

        # Stage list section (occupies most of the panel)
        list_height = self.config.stage_list_height
        stage_names = list(self.stages.keys())
        for i in range(list_height):
            idx = i + self.scroll_offset
            if idx < len(stage_names):
                stage_name = stage_names[idx]
                ctrl = self.stages[stage_name]
                status = "✓" if ctrl.enabled else "✗"
                sel = ">" if ctrl.selected else " "
                # Truncate to fit panel (leave room for ">✓ " prefix and padding)
                max_name_len = panel_width - 5
                display_name = ctrl.name[:max_name_len]
                line = f"│{sel}{status} {display_name:<{max_name_len}}"
                lines.append(line[:panel_width])
            else:
                lines.append("│" + " " * (panel_width - 2) + "│")

        # Separator
        lines.append("├" + "─" * (panel_width - 2) + "┤")

        # Parameter section (if stage selected)
        if self.selected_stage and self.selected_stage in self.stages:
            ctrl = self.stages[self.selected_stage]
            if ctrl.params:
                # Render each parameter as "name: [=====] value" with focus indicator
                for param_name, param_value in ctrl.params.items():
                    schema = ctrl.param_schema.get(param_name, {})
                    is_focused = param_name == self._focused_param
                    # Format value based on type
                    if isinstance(param_value, float):
                        val_str = f"{param_value:.2f}"
                    elif isinstance(param_value, int):
                        val_str = f"{param_value}"
                    elif isinstance(param_value, bool):
                        val_str = str(param_value)
                    else:
                        val_str = str(param_value)

                    # Build parameter line
                    if (
                        isinstance(param_value, (int, float))
                        and "min" in schema
                        and "max" in schema
                    ):
                        # Render as slider
                        min_val = schema["min"]
                        max_val = schema["max"]
                        # Normalize to 0-1 for bar length
                        if max_val != min_val:
                            ratio = (param_value - min_val) / (max_val - min_val)
                        else:
                            ratio = 0
                        bar_width = (
                            panel_width - len(param_name) - len(val_str) - 10
                        )  # approx space for "[] : ="
                        if bar_width < 1:
                            bar_width = 1
                        filled = int(round(ratio * bar_width))
                        bar = "[" + "=" * filled + " " * (bar_width - filled) + "]"
                        param_line = f"│ {param_name}: {bar} {val_str}"
                    else:
                        # Simple name=value
                        param_line = f"│ {param_name}={val_str}"

                    # Highlight focused parameter
                    if is_focused:
                        # Invert colors conceptually - for now use > prefix
                        param_line = "│> " + param_line[2:]

                    # Truncate to fit panel width
                    if len(param_line) > panel_width - 1:
                        param_line = param_line[: panel_width - 1]
                    lines.append(param_line + "│")
            else:
                lines.append("│ (no params)".ljust(panel_width - 1) + "│")
        else:
            lines.append("│ (select a stage)".ljust(panel_width - 1) + "│")

        # Info line before footer
        info_parts = []
        if self._current_preset:
            info_parts.append(f"Preset: {self._current_preset}")
        if self._presets:
            info_parts.append("[P] presets")
        info_str = " | ".join(info_parts) if info_parts else ""
        if info_str:
            padded = info_str.ljust(panel_width - 2)
            lines.append("│" + padded + "│")

        # Footer with instructions
        footer_line = self._render_footer(panel_width)
        lines.append(footer_line)

        # Ensure all lines are exactly panel_width
        return [line.ljust(panel_width)[:panel_width] for line in lines]

    def _render_footer(self, width: int) -> str:
        """Render footer with key hints."""
        if width >= 40:
            # Show preset name and key hints
            preset_info = (
                f"Preset: {self._current_preset}" if self._current_preset else ""
            )
            hints = " [S]elect [Space]UI [Tab]Params [Arrows/HJKL]Adjust "
            if self._presets:
                hints += "[P]Preset "
            combined = f"{preset_info}{hints}"
            if len(combined) > width - 4:
                combined = combined[: width - 4]
            footer = "└" + "─" * (width - 2) + "┘"
            return footer  # Just the line, we'll add info above in render
        else:
            return "└" + "─" * (width - 2) + "┘"

    def execute_command(self, command: dict) -> bool:
        """Execute a command from external control (e.g., WebSocket).

        Supported UI commands:
        - {"action": "toggle_stage", "stage": "stage_name"}
        - {"action": "select_stage", "stage": "stage_name"}
        - {"action": "adjust_param", "stage": "stage_name", "param": "param_name", "delta": 0.1}
        - {"action": "change_preset", "preset": "preset_name"}
        - {"action": "cycle_preset", "direction": 1}

        Pipeline Mutation commands are handled by the WebSocket/runner handler:
        - {"action": "add_stage", "stage": "stage_name", "type": "source|display|camera|effect"}
        - {"action": "remove_stage", "stage": "stage_name"}
        - {"action": "replace_stage", "stage": "old_stage_name", "with": "new_stage_type"}
        - {"action": "swap_stages", "stage1": "name1", "stage2": "name2"}
        - {"action": "move_stage", "stage": "stage_name", "after": "other_stage"|"before": "other_stage"}
        - {"action": "enable_stage", "stage": "stage_name"}
        - {"action": "disable_stage", "stage": "stage_name"}
        - {"action": "cleanup_stage", "stage": "stage_name"}
        - {"action": "can_hot_swap", "stage": "stage_name"}

        Returns:
            True if command was handled, False if not
        """
        action = command.get("action")

        if action == "toggle_stage":
            stage_name = command.get("stage")
            if stage_name in self.stages:
                self.toggle_stage(stage_name)
                self._emit_event(
                    "stage_toggled",
                    stage_name=stage_name,
                    enabled=self.stages[stage_name].enabled,
                )
                return True

        elif action == "select_stage":
            stage_name = command.get("stage")
            if stage_name in self.stages:
                self.select_stage(stage_name)
                self._emit_event("stage_selected", stage_name=stage_name)
                return True

        elif action == "adjust_param":
            stage_name = command.get("stage")
            param_name = command.get("param")
            delta = command.get("delta", 0.1)
            if stage_name == self.selected_stage and param_name:
                self._focused_param = param_name
                self.adjust_selected_param(delta)
                self._emit_event(
                    "param_changed",
                    stage_name=stage_name,
                    param_name=param_name,
                    value=self.stages[stage_name].params.get(param_name),
                )
                return True

        elif action == "change_preset":
            preset_name = command.get("preset")
            if preset_name in self._presets:
                self._current_preset = preset_name
                self._emit_event("preset_changed", preset_name=preset_name)
                return True

        elif action == "cycle_preset":
            direction = command.get("direction", 1)
            self.cycle_preset(direction)
            return True

        return False

    def process_key_event(self, key: str | int, modifiers: int = 0) -> bool:
        """Process a keyboard event.

        Args:
            key: Key symbol (e.g., ' ', 's', pygame.K_UP, etc.)
            modifiers: Modifier bits (Shift, Ctrl, Alt)

        Returns:
            True if event was handled, False if not
        """
        # Normalize to string for simplicity
        key_str = self._normalize_key(key, modifiers)

        # Space: toggle UI panel visibility (only when preset picker not active)
        if key_str == " " and not self._show_preset_picker:
            self._show_panel = not getattr(self, "_show_panel", True)
            return True

        # Space: toggle UI panel visibility (only when preset picker not active)
        if key_str == " " and not self._show_preset_picker:
            self._show_panel = not getattr(self, "_show_panel", True)
            return True

        # S: select stage (cycle)
        if key_str == "s" and modifiers == 0:
            stages = list(self.stages.keys())
            if not stages:
                return False
            if self.selected_stage:
                current_idx = stages.index(self.selected_stage)
                next_idx = (current_idx + 1) % len(stages)
            else:
                next_idx = 0
            self.select_stage(stages[next_idx])
            return True

        # P: toggle preset picker (only when panel is visible)
        if key_str == "p" and self._show_panel:
            self._show_preset_picker = not self._show_preset_picker
            if self._show_preset_picker:
                self._preset_scroll_offset = 0
            return True

        # HJKL or Arrow Keys: scroll stage list, preset list, or adjust param
        # vi-style: K=up, J=down (J is actually next line in vi, but we use for down)
        # We'll use J for down, K for up, H for left, L for right
        elif key_str in ("up", "down", "kp8", "kp2", "j", "k"):
            # If preset picker is open, scroll preset list
            if self._show_preset_picker:
                delta = -1 if key_str in ("up", "kp8", "k") else 1
                self._preset_scroll_offset = max(0, self._preset_scroll_offset + delta)
                # Ensure scroll doesn't go past end
                max_offset = max(0, len(self._presets) - 1)
                self._preset_scroll_offset = min(max_offset, self._preset_scroll_offset)
                return True
            # If param is focused, adjust param value
            elif self.selected_stage and self._focused_param:
                delta = -1.0 if key_str in ("up", "kp8", "k") else 1.0
                self.adjust_selected_param(delta)
                return True
            # Otherwise scroll stages
            else:
                delta = -1 if key_str in ("up", "kp8", "k") else 1
                self.scroll_stages(delta)
                return True

        # Left/Right or H/L: adjust param (if param selected)
        elif key_str in ("left", "right", "kp4", "kp6", "h", "l"):
            if self.selected_stage:
                delta = -0.1 if key_str in ("left", "kp4", "h") else 0.1
                self.adjust_selected_param(delta)
                return True

        # Tab: cycle through parameters
        if key_str == "tab" and self.selected_stage:
            ctrl = self.stages[self.selected_stage]
            param_names = list(ctrl.params.keys())
            if param_names:
                if self._focused_param in param_names:
                    current_idx = param_names.index(self._focused_param)
                    next_idx = (current_idx + 1) % len(param_names)
                else:
                    next_idx = 0
                self._focused_param = param_names[next_idx]
                return True

        # Preset picker navigation
        if self._show_preset_picker:
            # Enter: select currently highlighted preset
            if key_str == "return":
                if self._presets:
                    idx = self._preset_scroll_offset
                    if idx < len(self._presets):
                        self._current_preset = self._presets[idx]
                        self._emit_event(
                            "preset_changed", preset_name=self._current_preset
                        )
                        self._show_preset_picker = False
                        return True
            # Escape: close picker without changing
            elif key_str == "escape":
                self._show_preset_picker = False
                return True

        # Escape: deselect stage (only when picker not active)
        elif key_str == "escape" and self.selected_stage:
            self.selected_stage = None
            for ctrl in self.stages.values():
                ctrl.selected = False
            self._focused_param = None
            return True

        return False

    def _normalize_key(self, key: str | int, modifiers: int) -> str:
        """Normalize key to a string identifier."""
        # Handle pygame keysyms if imported
        try:
            import pygame

            if isinstance(key, int):
                # Map pygame constants to strings
                key_map = {
                    pygame.K_UP: "up",
                    pygame.K_DOWN: "down",
                    pygame.K_LEFT: "left",
                    pygame.K_RIGHT: "right",
                    pygame.K_SPACE: " ",
                    pygame.K_ESCAPE: "escape",
                    pygame.K_s: "s",
                    pygame.K_w: "w",
                    # HJKL navigation (vi-style)
                    pygame.K_h: "h",
                    pygame.K_j: "j",
                    pygame.K_k: "k",
                    pygame.K_l: "l",
                }
                # Check for keypad keys with KP prefix
                if hasattr(pygame, "K_KP8") and key == pygame.K_KP8:
                    return "kp8"
                if hasattr(pygame, "K_KP2") and key == pygame.K_KP2:
                    return "kp2"
                if hasattr(pygame, "K_KP4") and key == pygame.K_KP4:
                    return "kp4"
                if hasattr(pygame, "K_KP6") and key == pygame.K_KP6:
                    return "kp6"
                return key_map.get(key, f"pygame_{key}")
        except ImportError:
            pass

        # Already a string?
        if isinstance(key, str):
            return key.lower()

        return str(key)

    def set_event_callback(self, event_type: str, callback: Callable) -> None:
        """Register a callback for UI events.

        Args:
            event_type: Event type ("stage_toggled", "param_changed", "stage_selected", "preset_changed")
            callback: Function to call when event occurs
        """
        self._callbacks[event_type] = callback

    def _emit_event(self, event_type: str, **data) -> None:
        """Emit an event to registered callbacks."""
        callback = self._callbacks.get(event_type)
        if callback:
            try:
                callback(**data)
            except Exception:
                pass

    def set_presets(self, presets: list[str], current: str) -> None:
        """Set available presets and current selection.

        Args:
            presets: List of preset names
            current: Currently active preset name
        """
        self._presets = presets
        self._current_preset = current

    def cycle_preset(self, direction: int = 1) -> str:
        """Cycle to next/previous preset.

        Args:
            direction: 1 for next, -1 for previous

        Returns:
            New preset name
        """
        if not self._presets:
            return self._current_preset
        try:
            current_idx = self._presets.index(self._current_preset)
        except ValueError:
            current_idx = 0
        next_idx = (current_idx + direction) % len(self._presets)
        self._current_preset = self._presets[next_idx]
        self._emit_event("preset_changed", preset_name=self._current_preset)
        return self._current_preset

    def _render_preset_picker(self, panel_width: int) -> list[str]:
        """Render a full-screen preset picker overlay."""
        lines = []
        picker_height = min(len(self._presets) + 2, self.config.stage_list_height)
        # Create a centered box
        title = " Select Preset "
        box_width = min(40, panel_width - 2)
        lines.append("┌" + "─" * (box_width - 2) + "┐")
        lines.append("│" + title.center(box_width - 2) + "│")
        lines.append("├" + "─" * (box_width - 2) + "┤")
        # List presets with selection
        visible_start = self._preset_scroll_offset
        visible_end = visible_start + picker_height - 2
        for i in range(visible_start, min(visible_end, len(self._presets))):
            preset_name = self._presets[i]
            is_current = preset_name == self._current_preset
            prefix = "▶ " if is_current else "  "
            line = f"│ {prefix}{preset_name}"
            if len(line) < box_width - 1:
                line = line.ljust(box_width - 1)
            lines.append(line[: box_width - 1] + "│")
        # Footer with help
        help_text = "[P] close [↑↓] navigate [Enter] select"
        footer = "├" + "─" * (box_width - 2) + "┤"
        lines.append(footer)
        lines.append("│" + help_text.center(box_width - 2) + "│")
        lines.append("└" + "─" * (box_width - 2) + "┘")
        return lines
