"""Network connectivity monitoring widget.

This widget subscribes to the 'connectivity' topic to receive
real-time ping results from the backend. The backend handles all
ICMP ping operations and emits CONNECTIVITY_UPDATE events.
"""

from __future__ import annotations

from collections import deque
from typing import TYPE_CHECKING, Any, Literal, Protocol

from textual.app import ComposeResult
from textual.containers import Container
from textual.dom import NoMatches
from textual.message import Message
from textual.reactive import var
from textual.widgets import DataTable

if TYPE_CHECKING:
    pass

StateTypes = Literal["Unknown", "Connected", "Degraded", "Disconnected"]


class NetworkClient(Protocol):
    """Protocol for network RPC client."""

    async def test_connectivity(
        self, gateways: list[str], include_public: bool = True
    ) -> dict[str, Any]: ...

    async def subscribe(self, topics: list[str]) -> dict[str, Any]: ...

    async def unsubscribe(self, topics: list[str]) -> dict[str, Any]: ...


class Connectivity(Container):
    """Displays network connectivity status via ping monitoring.

    Subscribes to the 'connectivity' topic to receive real-time
    ICMP ping results from the backend.
    """

    class ConnectivityStateChanged(Message):
        """Posted when connectivity state changes."""

        def __init__(
            self, state: StateTypes, edge_transition: bool | None = None
        ) -> None:
            super().__init__()
            self.state = state
            self.edge_transition = edge_transition

    class GatewayValidation(Message):
        """Posted when a gateway's routeability is validated."""

        def __init__(self, gateway: str, routeable: bool) -> None:
            super().__init__()
            self.gateway = gateway
            self.routeable = routeable

    BORDER_TITLE = "Connectivity"

    state: var[StateTypes] = var("Unknown")

    def __init__(self, client: NetworkClient | None = None) -> None:
        """Initialize the connectivity widget.

        Args:
            client: RPC client for network operations
        """
        super().__init__()
        self.client = client
        # Host -> list of recent ping results
        self.ping_results: dict[str, deque[dict[str, Any]]] = {}
        # Host -> {total, missed}
        self.ping_counts: dict[str, dict[str, int]] = {}
        self._monitoring = False

    def compose(self) -> ComposeResult:
        columns: list[dict[str, str | int]] = [
            {"label": "Host", "key": "host", "width": 15},
            {"label": "  Loss", "key": "loss", "width": 6},
            {"label": "Total", "key": "total", "width": 5},
            {"label": "    Last RTT", "key": "last_rtt", "width": 12},
            {"label": " Average RTT", "key": "average_rtt", "width": 12},
            {"label": "Error", "key": "error", "width": 16},
        ]

        dt: DataTable = DataTable(show_cursor=False)
        for column in columns:
            dt.add_column(**column)

        yield dt

    async def on_mount(self) -> None:
        """Subscribe to connectivity topic on mount."""
        if self.client:
            try:
                await self.client.subscribe(["connectivity"])
                self._monitoring = True
            except Exception:
                pass

    async def on_unmount(self) -> None:
        """Unsubscribe from connectivity topic on unmount."""
        if self.client and self._monitoring:
            try:
                await self.client.unsubscribe(["connectivity"])
            except Exception:
                pass

    def handle_connectivity_event(self, data: dict[str, Any]) -> None:
        """Handle a connectivity update event from the backend.

        Args:
            data: Event data with gateway, reachable, latency_ms, packet_loss
        """
        gateway = data.get("gateway", "")
        if not gateway:
            return

        # Initialize tracking for new gateways
        if gateway not in self.ping_results:
            self.ping_results[gateway] = deque(maxlen=5)
            self.ping_counts[gateway] = {"total": 0, "missed": 0}
            self._add_table_row(gateway)

        # Store result
        self.ping_results[gateway].append(data)
        self.ping_counts[gateway]["total"] += 1
        if not data.get("reachable", False):
            self.ping_counts[gateway]["missed"] += 1

        # Update display
        self._update_row(gateway)
        self._evaluate_state()

    def start_monitoring(self, gateways: list[str]) -> None:
        """Start monitoring the specified gateways.

        Args:
            gateways: List of gateway IPs to monitor
        """
        default_hosts = ["1.1.1.1", "8.8.8.8"]
        hosts = [*gateways, *default_hosts]

        self.state = "Unknown"
        self.ping_results.clear()
        self.ping_counts.clear()

        self._set_table_hosts(hosts)

    def _set_table_hosts(self, hosts: list[str]) -> None:
        """Set up the table with host rows."""
        try:
            table = self.query_one(DataTable)
        except NoMatches:
            return

        table.clear()

        for host in hosts:
            self.ping_results[host] = deque(maxlen=5)
            self.ping_counts[host] = {"total": 0, "missed": 0}
            row = [host, "  0.0%", "0", "           -", "           -", ""]
            table.add_row(*row, key=host)

    def _add_table_row(self, host: str) -> None:
        """Add a single host row to the table."""
        try:
            table = self.query_one(DataTable)
        except NoMatches:
            return

        row = [host, "  0.0%", "0", "           -", "           -", ""]
        table.add_row(*row, key=host)

    def _update_row(self, host: str) -> None:
        """Update a host row with current data."""
        try:
            table = self.query_one(DataTable)
        except NoMatches:
            return

        results = self.ping_results.get(host, deque())
        if not results:
            return

        last_res = results[-1]
        count = self.ping_counts[host]

        # Calculate average RTT
        valid_rtts = [r.get("latency_ms") for r in results if r.get("latency_ms")]
        if valid_rtts:
            avg = sum(valid_rtts) / len(valid_rtts)
            average_rtt = str.rjust(f"{avg:.3f} ms", 12)
        else:
            average_rtt = "           -"

        # Last RTT
        if last_res.get("latency_ms"):
            last_rtt = str.rjust(f"{last_res['latency_ms']:.3f} ms", 12)
        else:
            last_rtt = "           -"

        # Packet loss
        total = count["total"]
        if total > 0:
            loss_pct = (count["missed"] / total) * 100
            loss = str.rjust(f"{loss_pct:.2f}%", 6)
        else:
            loss = "  0.0%"

        # Error message
        error = last_res.get("error", "") if not last_res.get("reachable") else "-"

        table.update_cell(row_key=host, column_key="average_rtt", value=average_rtt)
        table.update_cell(row_key=host, column_key="last_rtt", value=last_rtt)
        table.update_cell(row_key=host, column_key="total", value=str(total))
        table.update_cell(row_key=host, column_key="loss", value=loss)
        table.update_cell(row_key=host, column_key="error", value=error)

    def _evaluate_state(self) -> None:
        """Compute overall connectivity state from ping results."""
        cloudflare = self.ping_counts.get("1.1.1.1", {})
        google = self.ping_counts.get("8.8.8.8", {})

        if not cloudflare or not google:
            return

        cf_results = self.ping_results.get("1.1.1.1", deque())
        g_results = self.ping_results.get("8.8.8.8", deque())

        cf_success = [r for r in cf_results if r.get("reachable")]
        g_success = [r for r in g_results if r.get("reachable")]

        cf_total = cloudflare.get("total", 0)
        g_total = google.get("total", 0)

        cf_loss = cloudflare.get("missed", 0) / cf_total if cf_total else 0.0
        g_loss = google.get("missed", 0) / g_total if g_total else 0.0

        state: StateTypes = "Unknown"

        if not cf_success and not g_success:
            state = "Disconnected"
        elif not cf_success or not g_success:
            state = "Degraded"
        elif cf_loss < 0.3 and g_loss < 0.3:
            state = "Connected"
        elif cf_loss >= 0.3 or g_loss >= 0.3:
            state = "Degraded"

        self.state = state

    def watch_state(self, old: StateTypes, new: StateTypes) -> None:
        """Post message when state changes."""
        if old == new:
            return

        edge = None

        # Track edge transitions (connect/disconnect)
        if old == "Disconnected" and new == "Degraded":
            edge = True
        elif old == "Degraded" and new == "Disconnected":
            edge = False

        self.post_message(self.ConnectivityStateChanged(new, edge))
