"""Tree-based hierarchical display backend."""

from __future__ import annotations

import threading
import time
from dataclasses import dataclass, field
from typing import Any

from rich.console import Console
from rich.live import Live
from rich.text import Text
from rich.tree import Tree

from hil_testbench.data_structs.parameters import ParametersSchema
from hil_testbench.display.widgets.base import VisualizationMode, WidgetContext
from hil_testbench.display.widgets.parameter import ParameterValueWidget
from hil_testbench.display.widgets.status import StatusWidget
from hil_testbench.display.widgets.task_status import TaskStatusWidget
from hil_testbench.display.widgets.trendline import TrendlineData, TrendlineWidget


@dataclass
class CommandData:
    """Tracks individual command execution data."""

    name: str
    status: str = "not_started"  # not_started, running, completed, failed
    start_time: float | None = None
    end_time: float | None = None
    duration: float = 0.0
    error_count: int = 0
    errors: list[str] = field(default_factory=list)  # Recent error messages
    warnings: list[str] = field(default_factory=list)  # Recent warning messages
    parameter_count: int = 0  # Parameters received for this command's output


@dataclass
class TaskData:
    """Enhanced task tracking with command hierarchy."""

    name: str
    status: str = "not_started"
    start_time: float | None = None
    end_time: float | None = None
    error_count: int = 0
    parameters: dict[str, Any] = field(default_factory=dict)
    trendlines: dict[str, TrendlineData] = field(default_factory=dict)
    commands: dict[str, CommandData] = field(default_factory=dict)
    elapsed_time: float = 0
    total_duration: float = 60
    last_parameter_update: float | None = None  # Track last time we received data
    errors: list[str] = field(default_factory=list)  # Recent error messages
    warnings: list[str] = field(default_factory=list)  # Recent warning messages
    parameter_count: int = 0  # Total parameter events received


class RichTreeBackend:
    """Tree-based hierarchical display using Rich Live rendering."""

    def __init__(self, config: Any, logger: Any) -> None:
        self._console = Console()
        self._tasks: dict[str, TaskData] = {}
        self._schemas: dict[str, ParametersSchema] = {}
        self._tree: Tree | None = None
        self._live: Live | None = None

        # Read visualization mode from config
        viz_mode_str = "normal"  # default
        if hasattr(config, "display") and isinstance(config.display, dict):
            viz_mode_str = config.display.get("visualization_mode", "normal")

        # Map string to enum
        mode_map = {
            "minimal": VisualizationMode.MINIMAL,
            "normal": VisualizationMode.NORMAL,
            "full": VisualizationMode.FULL,
        }
        self._mode = mode_map.get(viz_mode_str.lower(), VisualizationMode.NORMAL)

        self._config = config
        self.logger = logger
        self._last_update = 0.0
        self._update_interval = 0.5  # Update tree every 500ms max

        # Calculate staleness threshold from sampling interval
        # Default: 3x the interval (to allow for processing delays)
        self._stale_multiplier = 3.0
        interval = float(getattr(config, "interval", 5))
        self._stale_threshold = interval * self._stale_multiplier

        # Background update thread for staleness detection
        self._update_thread: threading.Thread | None = None
        self._stop_thread = threading.Event()
        self._thread_interval = max(
            1.0, interval / 2
        )  # Update at least 2x per sampling interval

    def initialize(self, task_names: list[str] | None = None) -> None:
        """Initialize task tracking."""
        if task_names:
            for name in task_names:
                self._tasks[name] = TaskData(name)

    def start_display(self) -> None:
        """Initialize tree and start Live rendering."""
        self._tree = Tree("📊 [bold]Task Execution[/bold]")
        self._live = Live(
            self._tree,
            console=self._console,
            refresh_per_second=2,
            transient=False,
        )
        self._live.start()

        # Start background update thread for staleness detection
        self._stop_thread.clear()
        self._update_thread = threading.Thread(target=self._update_loop, daemon=True)
        self._update_thread.start()

    def register_command(
        self, task_name: str, command_name: str, start_time: float | None = None
    ) -> None:
        """Register a command under a task."""
        if task_name not in self._tasks:
            self._tasks[task_name] = TaskData(task_name)

        task = self._tasks[task_name]
        if command_name not in task.commands:
            task.commands[command_name] = CommandData(
                name=command_name, start_time=start_time
            )

    def update_parameter(
        self, task_name: str, param_name: str, event: dict[str, Any], timestamp: float
    ) -> None:
        """Update parameter and refresh tree."""
        if task_name not in self._tasks:
            self._tasks[task_name] = TaskData(task_name)

        task = self._tasks[task_name]

        # Extract value from event
        value = event.get("value")
        if value is None:
            return

        # Store parameter
        task.parameters[param_name] = value

        # Update last data timestamp and count
        task.last_parameter_update = timestamp
        task.parameter_count += 1

        # Update trendline data
        if param_name not in task.trendlines:
            task.trendlines[param_name] = TrendlineData(maxlen=10)
        task.trendlines[param_name].add(timestamp, value)

        # Update elapsed time
        if task.start_time:
            task.elapsed_time = timestamp - task.start_time

        # Throttle tree rebuilds
        current_time = time.time()
        if current_time - self._last_update >= self._update_interval:
            self._rebuild_tree()
            self._last_update = current_time

    def update_task_status(
        self, task_name: str, status: str, start_time: float | None = None
    ) -> None:
        """Update task status."""
        if task_name not in self._tasks:
            self._tasks[task_name] = TaskData(task_name)

        task = self._tasks[task_name]
        task.status = status
        if start_time is not None:
            task.start_time = start_time
        if status in ("completed", "failed"):
            task.end_time = time.time()

            # Framework validation: If task "completed" but produced no data, mark as failed
            if status == "completed" and task.parameter_count == 0 and task.start_time:
                elapsed = task.end_time - task.start_time
                # Only validate if task ran for more than 2 seconds (avoid false positives for quick tasks)
                if elapsed > 2.0:
                    task.status = "failed"
                    task.errors.append(
                        f"Task completed but produced no data (ran {elapsed:.0f}s)"
                    )
                    task.error_count += 1

        self._rebuild_tree()

    def update_command_status(
        self, task_name: str, command_name: str, status: str
    ) -> None:
        """Update individual command status."""
        if task_name not in self._tasks:
            self._tasks[task_name] = TaskData(task_name)

        task = self._tasks[task_name]
        if command_name not in task.commands:
            task.commands[command_name] = CommandData(command_name)

        cmd = task.commands[command_name]
        cmd.status = status

        if status == "running" and cmd.start_time is None:
            cmd.start_time = time.time()
        elif status in ("completed", "failed"):
            cmd.end_time = time.time()
            if cmd.start_time:
                cmd.duration = cmd.end_time - cmd.start_time

            # Command validation: If command completed but produced no output data
            if (
                status == "completed"
                and cmd.parameter_count == 0
                and cmd.duration > 2.0
            ):
                # Mark as warning rather than changing status (since OS exit code was 0)
                cmd.warnings.append(f"No data output (ran {cmd.duration:.0f}s)")

        self._rebuild_tree()

    def report_task_error(self, task_name: str, error: str) -> None:
        """Report an error."""
        if task_name not in self._tasks:
            self._tasks[task_name] = TaskData(task_name)

        task = self._tasks[task_name]
        task.error_count += 1

        # Store error message (keep last 3 errors)
        truncated = error[:100] + "..." if len(error) > 100 else error
        task.errors.append(truncated)
        if len(task.errors) > 3:
            task.errors.pop(0)

        self._rebuild_tree()

    def add_log_line(self, message: str, level: str = "INFO") -> None:
        """Add a log line - capture warnings/errors."""
        # Try to extract task name from message context
        # For now, we'll skip this since we don't have task context in add_log_line
        # Errors are better tracked via report_task_error which has task_name
        pass

    def update_display(self) -> None:
        """Force tree rebuild."""
        self._rebuild_tree()

    def _rebuild_tree(self) -> None:
        """Rebuild entire tree structure."""
        if not self._tree or not self._live:
            return

        # Create new tree
        new_tree = Tree("📊 [bold]Task Execution[/bold]")
        context = WidgetContext(mode=self._mode)
        current_time = time.time()

        for task_name, task_data in self._tasks.items():
            # Render task node using composite widget
            primary_param = self._get_primary_param(task_data)

            # Update duration
            duration = 60  # default
            if self._config and hasattr(self._config, "duration"):
                duration = float(self._config.duration or 60)
            task_data.total_duration = duration

            # Update elapsed time
            if task_data.start_time:
                task_data.elapsed_time = current_time - task_data.start_time

            # Check for stale data (only for running tasks)
            effective_status = task_data.status
            if task_data.status == "running" and task_data.last_parameter_update:
                time_since_update = current_time - task_data.last_parameter_update
                if time_since_update > self._stale_threshold:
                    # Override status to show degraded state
                    effective_status = "stale"

            # Get trendline data for primary param
            trendline_data = None
            if primary_param and primary_param[0] in task_data.trendlines:
                trendline_data = task_data.trendlines[primary_param[0]]

            schema = self._schemas.get(task_name)
            task_widget = TaskStatusWidget(
                task_name=task_name,
                status=effective_status,
                primary_param=primary_param,
                trendline_data=trendline_data,
                elapsed=task_data.elapsed_time,
                duration=task_data.total_duration,
                error_count=task_data.error_count,
                schema=schema,
            )

            task_node = new_tree.add(task_widget.render(context).rich_text)

            # Add task-level errors/warnings
            for error in task_data.errors:
                task_node.add(Text(f"  ❌ {error}", style="red dim"))
            for warning in task_data.warnings:
                task_node.add(Text(f"  ⚠️  {warning}", style="yellow dim"))

            # Add command nodes (normal/full mode only)
            if self._mode != VisualizationMode.MINIMAL and task_data.commands:
                for cmd_name, cmd_data in task_data.commands.items():
                    # Status icon
                    status_widget = StatusWidget(cmd_data.status)
                    status_text = status_widget.render(context).rich_text

                    # Command name and duration
                    cmd_text = Text.assemble(
                        status_text,
                        Text(f" {cmd_name}", style="dim"),
                    )

                    # Add duration if completed
                    if cmd_data.duration > 0:
                        cmd_text.append(
                            Text(f" ({cmd_data.duration:.0f}s)", style="dim cyan")
                        )
                    elif cmd_data.status == "running" and cmd_data.start_time:
                        elapsed = time.time() - cmd_data.start_time
                        cmd_text.append(Text(f" ({elapsed:.0f}s)", style="dim yellow"))

                    cmd_node = task_node.add(cmd_text)

                    # Add command-level errors/warnings
                    for error in cmd_data.errors:
                        cmd_node.add(Text(f"    ❌ {error}", style="red dim"))
                    for warning in cmd_data.warnings:
                        cmd_node.add(Text(f"    ⚠️  {warning}", style="yellow dim"))

            # Add all parameters (full mode only)
            if self._mode == VisualizationMode.FULL and task_data.parameters:
                param_node = task_node.add(Text("Parameters:", style="dim bold"))
                for param_name, param_value in task_data.parameters.items():
                    field = self._get_field_from_schema(task_name, param_name)
                    if field:
                        value_widget = ParameterValueWidget(param_value, field)
                        value_text = value_widget.render(context).rich_text

                        # Add trendline if available
                        trendline_text = Text("")
                        if param_name in task_data.trendlines:
                            trendline_widget = TrendlineWidget(
                                task_data.trendlines[param_name]
                            )
                            trendline_text = trendline_widget.render(context).rich_text

                        param_text = Text.assemble(
                            Text(f"  {param_name}: ", style="dim"),
                            value_text,
                            Text(" "),
                            trendline_text,
                        )
                        param_node.add(param_text)

        # Update live display
        self._live.update(new_tree)

    def shutdown(self) -> None:
        """Stop live display and print final tree."""
        # Stop background update thread
        self._stop_thread.set()
        if self._update_thread and self._update_thread.is_alive():
            self._update_thread.join(timeout=2.0)

        # Force final update before stopping
        self._rebuild_tree()

        if self._live:
            self._live.stop()
            # Tree remains visible due to transient=False

        # Print summary
        self._console.print("\n[bold]Task Summary:[/bold]")
        total_tasks = len(self._tasks)
        completed = sum(
            1
            for t in self._tasks.values()
            if t.status == "completed" and t.error_count == 0
        )
        failed = total_tasks - completed
        total_errors = sum(t.error_count for t in self._tasks.values())

        # Always print summary stats
        self._console.print(
            f"{total_tasks} task{'s' if total_tasks != 1 else ''} | "
            f"[green]{completed} passed[/green] | "
            f"[red]{failed} failed[/red]"
            + (f" | [yellow]{total_errors} errors[/yellow]" if total_errors > 0 else "")
        )

    def set_schema(self, task_name: str, schema: ParametersSchema) -> None:
        """Set the schema for a task."""
        self._schemas[task_name] = schema

    def set_visualization_mode(self, mode: VisualizationMode) -> None:
        """Set visualization detail level."""
        self._mode = mode
        self._rebuild_tree()

    def _update_loop(self) -> None:
        """Background thread for periodic updates to detect staleness."""
        while not self._stop_thread.is_set():
            # Rebuild tree to update elapsed time and check staleness
            self._rebuild_tree()

            # Sleep until next update
            self._stop_thread.wait(self._thread_interval)

    def _get_primary_param(self, task: TaskData) -> tuple[str, float] | None:
        """Get primary parameter from schema."""
        schema = self._schemas.get(task.name)
        if not schema:
            if task.parameters:
                first_key = next(iter(task.parameters.keys()))
                return (first_key, task.parameters[first_key])
            return None

        # Find field with is_primary=True
        for fld in schema.fields:
            if getattr(fld, "is_primary", False):
                if fld.name in task.parameters:
                    return (fld.name, task.parameters[fld.name])

        # Fallback: first display_enabled field
        for fld in schema.fields:
            if fld.display_enabled and fld.name in task.parameters:
                return (fld.name, task.parameters[fld.name])

        # Final fallback
        if task.parameters:
            first_key = next(iter(task.parameters.keys()))
            return (first_key, task.parameters[first_key])

        return None

    def _get_field_from_schema(self, task_name: str, param_name: str) -> Any | None:
        """Get parameter field from schema."""
        schema = self._schemas.get(task_name)
        if not schema:
            return None

        return next((f for f in schema.fields if f.name == param_name), None)
