"""LLMHosts TUI dashboard -- custom Textual widgets.

Each widget is a self-contained panel that accepts data via an ``update_data``
method, re-renders with Rich markup, and gracefully handles missing data.
"""

from __future__ import annotations

import logging
from collections import deque
from datetime import datetime  # noqa: TC003 — NamedTuple needs this at runtime
from typing import TYPE_CHECKING, Any, NamedTuple

from rich.text import Text
from textual.reactive import reactive
from textual.widgets import Static

from llmhosts.dashboard.theme import (
    HEALTH_ICONS,
    backend_color,
    format_cost,
    format_latency,
    format_percent,
    format_rpm,
    format_savings_percent,
    format_tokens,
    format_uptime,
    route_label,
)

if TYPE_CHECKING:
    from llmhosts.health.monitor import BackendHealth
    from llmhosts.metrics.collector import MetricsSnapshot

logger = logging.getLogger(__name__)

# Maximum number of request-log entries to keep in memory for display.
_MAX_LOG_ENTRIES = 200


# ---------------------------------------------------------------------------
# Data carrier for a single request-log row
# ---------------------------------------------------------------------------


class RequestEntry(NamedTuple):
    """Lightweight data for one row in the request log."""

    timestamp: datetime
    model_requested: str
    model_used: str
    backend: str
    cached: bool
    cost: float
    latency_ms: float


# ---------------------------------------------------------------------------
# HeaderWidget
# ---------------------------------------------------------------------------


class HeaderWidget(Static):
    """Top bar: version string, uptime, and hotkey hints."""

    uptime_seconds: reactive[float] = reactive(0.0)

    def __init__(self, version: str = "0.1.0", **kwargs: Any) -> None:
        super().__init__(**kwargs)
        self._version = version

    def render(self) -> Text:
        uptime_str = format_uptime(self.uptime_seconds)
        txt = Text()
        txt.append("  LLMHosts ", style="bold bright_blue")
        txt.append(f"v{self._version}", style="dim bright_blue")
        # Right-align uptime and quit hint using padding
        right = f"Uptime: {uptime_str}     [Q]uit  [R]efresh  [D]etail"
        padding = max(1, 80 - len(txt.plain) - len(right))
        txt.append(" " * padding)
        txt.append("Uptime: ", style="dim white")
        txt.append(uptime_str, style="bold white")
        txt.append("     ", style="")
        txt.append("[Q]", style="bold yellow")
        txt.append("uit  ", style="dim white")
        txt.append("[R]", style="bold yellow")
        txt.append("efresh  ", style="dim white")
        txt.append("[D]", style="bold yellow")
        txt.append("etail  ", style="dim white")
        return txt

    def update_data(self, uptime_seconds: float) -> None:
        """Update the displayed uptime."""
        self.uptime_seconds = uptime_seconds


# ---------------------------------------------------------------------------
# SavingsWidget
# ---------------------------------------------------------------------------


class SavingsWidget(Static):
    """Panel showing total savings, percentage, and request count."""

    total_savings: reactive[float] = reactive(0.0)
    savings_percent: reactive[float] = reactive(0.0)
    total_requests: reactive[int] = reactive(0)
    total_cost: reactive[float] = reactive(0.0)

    def render(self) -> Text:
        txt = Text()
        txt.append("  SAVINGS\n", style="bold bright_blue")
        txt.append("\n")

        if self.total_requests == 0:
            txt.append("  Waiting for requests...\n", style="dim italic")
            txt.append("  No data yet\n", style="dim")
            return txt

        # Big savings number
        savings_str = format_cost(self.total_savings)
        txt.append(f"  {savings_str}", style="bold green")
        txt.append(" saved\n", style="green")

        # Percentage reduction
        pct_str = format_savings_percent(self.savings_percent)
        txt.append(f"  {pct_str}", style="bold green")
        txt.append(" reduction\n", style="dim white")

        # Request count
        txt.append(f"  {self.total_requests:,}", style="bold white")
        txt.append(" requests\n", style="dim white")

        # Actual cost
        if self.total_cost > 0:
            cost_str = format_cost(self.total_cost)
            txt.append(f"  {cost_str}", style="bold red")
            txt.append(" spent\n", style="dim white")

        return txt

    def update_data(self, snapshot: MetricsSnapshot) -> None:
        """Refresh widget from a metrics snapshot."""
        self.total_savings = snapshot.total_savings
        self.savings_percent = snapshot.savings_percent
        self.total_requests = snapshot.total_requests
        self.total_cost = snapshot.total_cost


# ---------------------------------------------------------------------------
# BackendsWidget
# ---------------------------------------------------------------------------


class BackendsWidget(Static):
    """Panel showing backend health status."""

    _backend_data: reactive[list[tuple[str, bool, int]]] = reactive(list, always_update=True)

    def render(self) -> Text:
        txt = Text()
        txt.append("  BACKENDS\n", style="bold bright_blue")
        txt.append("\n")

        data: list[tuple[str, bool, int]] = self._backend_data  # type: ignore[assignment]
        if not data:
            txt.append("  Discovering...\n", style="dim italic")
            txt.append("  No backends registered\n", style="dim")
            return txt

        for backend_type, is_healthy, model_count in data:
            icon = HEALTH_ICONS[is_healthy]
            color = backend_color(backend_type)
            status = "healthy" if is_healthy else "offline"
            status_color = "green" if is_healthy else "red"

            txt.append(f"  {icon} ")
            display_name = "Local" if backend_type == "ollama" else backend_type.capitalize()
            txt.append(display_name, style=f"bold {color}")

            if model_count > 0:
                txt.append(f": {model_count} model{'s' if model_count != 1 else ''}", style="dim white")
            txt.append(f" ({status})", style=status_color)
            txt.append("\n")

        return txt

    def update_backends(self, backends: list[tuple[str, bool, int]]) -> None:
        """Update backend list.

        Parameters
        ----------
        backends:
            List of ``(backend_type, is_healthy, model_count)`` tuples.
        """
        self._backend_data = list(backends)

    def update_from_health(self, statuses: dict[str, BackendHealth]) -> None:
        """Update from a ``HealthMonitor.get_all_status()`` dict."""
        entries: list[tuple[str, bool, int]] = []
        for bt, health in sorted(statuses.items()):
            entries.append((bt, health.is_healthy, 0))
        self._backend_data = entries


# ---------------------------------------------------------------------------
# RequestLogWidget
# ---------------------------------------------------------------------------


class RequestLogWidget(Static):
    """Scrollable list of recent request routing decisions."""

    _entries: reactive[int] = reactive(0)  # bump to trigger re-render

    def __init__(self, **kwargs: Any) -> None:
        super().__init__(**kwargs)
        self._log: deque[RequestEntry] = deque(maxlen=_MAX_LOG_ENTRIES)

    def render(self) -> Text:
        txt = Text()
        txt.append("  RECENT REQUESTS\n", style="bold bright_blue")

        if not self._log:
            txt.append("\n")
            txt.append("  No requests yet — send a chat completion to see routing decisions.\n", style="dim italic")
            return txt

        txt.append("\n")

        # Show header row
        txt.append("  TIME     ", style="dim white")
        txt.append("│", style="dim rgb(60,60,100)")
        txt.append(" REQUESTED       ", style="dim white")
        txt.append("→", style="dim yellow")
        txt.append(" ROUTED TO       ", style="dim white")
        txt.append("│", style="dim rgb(60,60,100)")
        txt.append(" TYPE  ", style="dim white")
        txt.append("│", style="dim rgb(60,60,100)")
        txt.append("  COST   ", style="dim white")
        txt.append("│", style="dim rgb(60,60,100)")
        txt.append(" LATENCY\n", style="dim white")

        # Separator
        txt.append("  " + "─" * 76 + "\n", style="dim rgb(60,60,100)")

        # Show most recent entries (newest first), limited to display area
        display_count = min(len(self._log), 15)
        entries = list(self._log)[-display_count:]
        for entry in reversed(entries):
            time_str = entry.timestamp.strftime("%H:%M:%S")
            label, color = route_label(entry.backend, entry.cached)
            cost_str = format_cost(entry.cost)
            latency_str = format_latency(entry.latency_ms)

            # Truncate model names for display
            req_model = _truncate(entry.model_requested, 15)
            used_model = _truncate(entry.model_used, 15)

            txt.append(f"  {time_str} ", style="dim white")
            txt.append("│", style="dim rgb(60,60,100)")
            txt.append(f" {req_model:<15s} ", style="white")
            txt.append("→", style="dim yellow")
            txt.append(f" {used_model:<15s} ", style=backend_color(entry.backend))
            txt.append("│", style="dim rgb(60,60,100)")
            txt.append(f" {label:<5s} ", style=f"bold {color}")
            txt.append("│", style="dim rgb(60,60,100)")
            txt.append(f" {cost_str:>7s} ", style="green" if entry.cost == 0 else "red")
            txt.append("│", style="dim rgb(60,60,100)")
            txt.append(f" {latency_str:>7s}", style="white")
            txt.append("\n")

        return txt

    def add_request(self, entry: RequestEntry) -> None:
        """Append a new request to the log and re-render."""
        self._log.append(entry)
        self._entries = len(self._log)  # bump reactive to trigger render

    def add_requests(self, entries: list[RequestEntry]) -> None:
        """Append multiple requests at once."""
        for e in entries:
            self._log.append(e)
        self._entries = len(self._log)

    def clear_log(self) -> None:
        """Clear all entries."""
        self._log.clear()
        self._entries = 0


# ---------------------------------------------------------------------------
# MetricsWidget
# ---------------------------------------------------------------------------


class MetricsWidget(Static):
    """Compact metrics bar: RPM, cache hit rate, local %, avg latency, tokens."""

    rpm: reactive[float] = reactive(0.0)
    cache_hit_rate: reactive[float] = reactive(0.0)
    avg_latency_ms: reactive[float] = reactive(0.0)
    total_tokens: reactive[int] = reactive(0)
    requests_by_backend: reactive[dict[str, int]] = reactive(dict, always_update=True)

    def render(self) -> Text:
        txt = Text()
        txt.append("  METRICS", style="bold bright_blue")

        # Calculate local percentage
        by_backend: dict[str, int] = self.requests_by_backend  # type: ignore[assignment]
        total_reqs = sum(by_backend.values()) if by_backend else 0
        local_reqs = by_backend.get("ollama", 0) + by_backend.get("cache", 0)
        local_pct = (local_reqs / total_reqs * 100) if total_reqs > 0 else 0.0

        # Lay out as two rows of three metrics each
        txt.append("\n\n")

        # Row 1
        txt.append("  RPM: ", style="dim white")
        txt.append(format_rpm(self.rpm), style="bold white")

        txt.append("      │ ", style="dim rgb(60,60,100)")
        txt.append("Cache: ", style="dim white")
        txt.append(format_percent(self.cache_hit_rate), style="bold magenta")
        txt.append(" hit", style="dim white")

        txt.append("    │ ", style="dim rgb(60,60,100)")
        txt.append("Avg: ", style="dim white")
        txt.append(format_latency(self.avg_latency_ms), style="bold white")

        txt.append("\n")

        # Row 2
        txt.append("  Local: ", style="dim white")
        txt.append(f"{local_pct:.0f}%", style="bold cyan")

        txt.append("    │ ", style="dim rgb(60,60,100)")
        txt.append("Tokens: ", style="dim white")
        txt.append(format_tokens(self.total_tokens), style="bold white")

        # Backend breakdown
        if by_backend:
            txt.append("   │ ", style="dim rgb(60,60,100)")
            for bt, count in sorted(by_backend.items()):
                color = backend_color(bt)
                txt.append(f"{bt}: ", style=f"dim {color}")
                txt.append(f"{count}", style=f"bold {color}")
                txt.append("  ", style="")

        txt.append("\n")

        return txt

    def update_data(self, snapshot: MetricsSnapshot) -> None:
        """Refresh from a metrics snapshot."""
        self.rpm = snapshot.requests_per_minute
        self.cache_hit_rate = snapshot.cache_hit_rate
        self.avg_latency_ms = snapshot.avg_latency_ms
        self.total_tokens = snapshot.total_tokens
        self.requests_by_backend = dict(snapshot.requests_by_backend)


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _truncate(text: str, max_len: int) -> str:
    """Truncate *text* to *max_len*, adding ellipsis if needed."""
    if len(text) <= max_len:
        return text
    return text[: max_len - 1] + "…"
