from __future__ import annotations

import time
from typing import ClassVar

from textual.app import App, ComposeResult
from textual.containers import Vertical
from textual.widgets import DataTable, Footer, Header, Label

from hytop.core.format import fmt_elapsed, fmt_window
from hytop.core.sorting import next_sort_field_index, sort_with_missing_last
from hytop.net.models import MonitorState
from hytop.net.service import apply_node_results, drain_pending_nodes
from hytop.net.sort import sort_net_keys_grouped, split_iface_key


def format_rate(value: float, iec: bool = False) -> str:
    if iec:
        units = ["B/s", "KiB/s", "MiB/s", "GiB/s", "TiB/s"]
        base = 1024.0
    else:
        units = ["B/s", "kB/s", "MB/s", "GB/s", "TB/s"]
        base = 1000.0
    output = float(value)
    idx = 0
    while output >= base and idx < len(units) - 1:
        output /= base
        idx += 1
    return f"{output:.2f} {units[idx]}"


def format_iface_name(name: str, link_state: str | None) -> str:
    if not link_state:
        return name
    normalized = link_state.strip().lower()
    is_down = normalized in {"down", "disabled", "init", "inactive"} or normalized.startswith(
        "down"
    )
    if is_down:
        return f"{name} (down)"
    return name


class NetMonitorApp(App[int]):
    """Textual TUI application for real-time network interface monitoring.

    Replaces the previous Rich Live + Table rendering loop. Network data
    is collected by background threads; this App polls the shared MonitorState
    on a timer and updates the DataTable in place, enabling native scrolling.
    """

    CSS = """
    Screen {
        layout: vertical;
    }

    #net-table {
        height: 1fr;
        border: none;
    }

    #error-label {
        height: auto;
        padding: 0 1;
        color: $warning;
    }

    #error-label.hidden {
        display: none;
    }
    """

    BINDINGS: ClassVar[list] = [
        ("q", "quit_with_code", "Quit"),
        ("ctrl+c", "quit_with_code", "Quit"),
        ("s", "next_sort_field", "Next Sort Field"),
        ("S", "toggle_sort_order", "Toggle Sort Order"),
        ("g", "group_sort", "Grouped Sort"),
    ]
    SORT_FIELDS: ClassVar[list[tuple[str, str]]] = [
        ("host", "Host"),
        ("mode", "Mode"),
        ("nic", "NIC"),
        ("rx_bps", "RX"),
        ("tx_bps", "TX"),
        ("rx_avg", "RX@window"),
        ("tx_avg", "TX@window"),
    ]

    def __init__(
        self,
        hosts: list[str],
        window: float,
        interval: float,
        timeout: float | None,
        state: MonitorState,
        started: float,
        iec: bool = False,
        **kwargs: object,
    ) -> None:
        super().__init__(**kwargs)
        self._hosts = hosts
        self._window = window
        self._interval = interval
        self._timeout = timeout
        self._state = state
        self._started = started
        self._iec = iec
        # Map (host, iface_key) -> DataTable row key for in-place updates.
        self._row_keys: dict[tuple[str, str], str] = {}
        # Last displayed key order; when unchanged we use update_cell to preserve scroll/cursor.
        self._ordered_keys: list[tuple[str, str]] = []
        self._cell_column_keys: list[str] = [
            "host",
            "mode",
            "device",
            "nic",
            "rx",
            "tx",
            "rx_avg",
            "tx_avg",
        ]
        self._sort_mode: str = "grouped"
        self._sort_desc: bool = False
        self._sort_field_index: int = 0

    # ------------------------------------------------------------------
    # Composition
    # ------------------------------------------------------------------

    def compose(self) -> ComposeResult:
        yield Header()
        with Vertical():
            yield DataTable(id="net-table", cursor_type="row")
            yield Label("", id="error-label")
        yield Footer()

    def on_mount(self) -> None:
        """Initialise columns and start the polling timer."""
        table: DataTable = self.query_one("#net-table", DataTable)
        table.add_column("Host", key="host")
        table.add_column("Mode", key="mode")
        table.add_column("Device", key="device")
        table.add_column("NIC", key="nic")
        table.add_column("RX", key="rx")
        table.add_column("TX", key="tx")
        table.add_column(f"RX@{fmt_window(self._window)}", key="rx_avg")
        table.add_column(f"TX@{fmt_window(self._window)}", key="tx_avg")
        render_interval = min(self._interval, 0.5)
        self.set_interval(render_interval, self._tick)

    # ------------------------------------------------------------------
    # Actions
    # ------------------------------------------------------------------

    def action_quit_with_code(self) -> None:
        """Quit with a user-interrupt exit code."""
        self.exit(130)

    def action_next_sort_field(self) -> None:
        """Cycle sortable fields and switch to metric sorting mode."""
        if self._sort_mode == "grouped":
            self._sort_mode = "metric"
        self._sort_field_index = next_sort_field_index(
            self._sort_field_index, len(self.SORT_FIELDS)
        )

    def action_toggle_sort_order(self) -> None:
        """Toggle ascending / descending metric sort order."""
        self._sort_desc = not self._sort_desc

    def action_group_sort(self) -> None:
        """Reset to grouped default ordering."""
        self._sort_mode = "grouped"
        self._sort_desc = False

    def _sort_status_text(self) -> str:
        if self._sort_mode == "grouped":
            return "sort=grouped(host->mode->nic)"
        _, label = self.SORT_FIELDS[self._sort_field_index]
        order = "desc" if self._sort_desc else "asc"
        return f"sort={label.lower()} {order}"

    # ------------------------------------------------------------------
    # Internal timer callback
    # ------------------------------------------------------------------

    def _tick(self) -> None:
        """Drain pending collector results and refresh the DataTable."""
        state = self._state

        apply_node_results(
            nodes=drain_pending_nodes(hosts=self._hosts, state=state),
            interval=self._interval,
            state=state,
        )

        now = time.monotonic()
        elapsed = now - self._started

        # ---- Update app title with runtime info ----
        self.title = (
            f"hytop net  |  interval={self._interval:.2f}s"
            f"  |  window={fmt_window(self._window)}"
            f"  |  elapsed={fmt_elapsed(elapsed)}"
            f"  |  {self._sort_status_text()}"
        )

        # ---- Rebuild rows ----
        key_list = sort_net_keys_grouped(state.monitored_keys, self._hosts)
        if self._sort_mode == "metric":
            field, _ = self.SORT_FIELDS[self._sort_field_index]

            def _metric_value(key: tuple[str, str]) -> float | str | None:
                if field == "host":
                    return key[0]
                history = state.histories.get(key)
                if field == "mode":
                    mode, _ = split_iface_key(key[1])
                    return mode
                if field == "nic":
                    _, nic = split_iface_key(key[1])
                    return nic
                if history is None:
                    return None
                latest_rate = history.latest()
                if latest_rate is None:
                    return None
                if field == "rx_bps":
                    return latest_rate.rx_bps
                if field == "tx_bps":
                    return latest_rate.tx_bps
                if field == "rx_avg":
                    return history.avg("rx_bps", self._window, now)
                if field == "tx_avg":
                    return history.avg("tx_bps", self._window, now)
                return None

            key_list = sort_with_missing_last(key_list, _metric_value, self._sort_desc)
        table: DataTable = self.query_one("#net-table", DataTable)

        # Build keys_to_display: keys we actually have data for.
        keys_to_display: list[tuple[str, str]] = []
        for key in key_list:
            history = state.histories.get(key)
            latest_counter = state.latest_counter_by_key.get(key)
            if history is None or latest_counter is None:
                continue
            latest_rate = history.latest()
            if latest_rate is None:
                continue
            keys_to_display.append(key)

        def _cell_values(key: tuple[str, str]) -> list[str]:
            history = state.histories.get(key)
            latest_counter = state.latest_counter_by_key.get(key)
            if history is None or latest_counter is None:
                return []
            latest_rate = history.latest()
            if latest_rate is None:
                return []
            host, iface_key = key
            mode, name = split_iface_key(iface_key)
            device_text = latest_counter.device_name or name
            nic_text = format_iface_name(name, latest_counter.link_state)
            stale = (now - latest_rate.ts) > self._window
            if stale:
                return [host, mode, device_text, nic_text, "-", "-", "-", "-"]
            rx_avg = history.avg("rx_bps", self._window, now)
            tx_avg = history.avg("tx_bps", self._window, now)
            return [
                host,
                mode,
                device_text,
                nic_text,
                format_rate(latest_rate.rx_bps, self._iec),
                format_rate(latest_rate.tx_bps, self._iec),
                format_rate(rx_avg, self._iec),
                format_rate(tx_avg, self._iec),
            ]

        if keys_to_display == self._ordered_keys:
            # Same order: incremental update to preserve scroll position and cursor.
            for key in keys_to_display:
                row_key = self._row_keys.get(key)
                if row_key is None:
                    continue
                values = _cell_values(key)
                if len(values) != len(self._cell_column_keys):
                    continue
                for col_key, val in zip(self._cell_column_keys, values, strict=True):
                    table.update_cell(row_key, col_key, val)
        else:
            # Order or set changed: full rebuild.
            table.clear(columns=False)
            self._row_keys.clear()
            self._ordered_keys = keys_to_display.copy()
            for key in keys_to_display:
                values = _cell_values(key)
                if not values:
                    continue
                host, iface_key = key
                row_key = f"{host}:{iface_key}"
                table.add_row(*values, key=row_key)
                self._row_keys[key] = row_key

        # ---- Update error label ----
        error_label: Label = self.query_one("#error-label", Label)
        if state.errors:
            parts = [f"{h}: {err}" for h, err in state.errors.items()]
            error_label.update("  ".join(parts))
            error_label.remove_class("hidden")
        else:
            error_label.update("")
            error_label.add_class("hidden")

        # ---- Check global timeout ----
        if self._timeout is not None and elapsed >= self._timeout:
            self.exit(124)
