"""Network configuration screen (RPC-based).

This screen provides network interface configuration, routing display,
DNS information, and UPnP port mapping. All operations are performed
via RPC calls to the backend daemon.
"""

from __future__ import annotations

from ipaddress import IPv4Address
from typing import Any, Literal, Protocol

from textual import events, on, work
from textual.app import ComposeResult
from textual.binding import Binding
from textual.containers import Horizontal
from textual.message import Message
from textual.reactive import var
from textual.screen import Screen
from textual.widgets import Button, Footer, Header, TabbedContent, TabPane, Tabs

from flux_networking_shared.tui.messages import FocusTabsRequested
from flux_networking_shared.tui.models.interface_config_result import (
    InterfaceConfigResult,
    InterfaceShapingPolicy,
)
from flux_networking_shared.tui.models.network import ResolvedDnsServer, Route
from flux_networking_shared.tui.models.upnp import FluxApiPorts, UpnpLease
from flux_networking_shared.tui.screens.confirm_exit import ConfirmExitScreen
from flux_networking_shared.tui.screens.interface_modal_screen import (
    InterfaceModalScreen,
)
from flux_networking_shared.tui.widgets.connectivity import Connectivity
from flux_networking_shared.tui.widgets.dns import Dns
from flux_networking_shared.tui.widgets.interface import Interface
from flux_networking_shared.tui.widgets.interface_group import (
    FocusButtonsRequested,
    InterfaceGroup,
)
from flux_networking_shared.tui.widgets.network_summary import NetworkSummary
from flux_networking_shared.tui.widgets.routing import Routing
from flux_networking_shared.tui.widgets.upnp_builder import UpnpBuilder


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

    # Interface management
    async def get_interfaces(self) -> dict[str, Any]: ...
    async def set_static_ip(
        self, interface: str, address: str, gateway: str | None, dns: list[str]
    ) -> dict[str, Any]: ...
    async def set_dhcp(self, interface: str) -> dict[str, Any]: ...
    async def set_disabled(self, interface: str) -> dict[str, Any]: ...
    async def create_vlan(self, parent: str, vlan_id: int) -> dict[str, Any]: ...
    async def delete_vlan(self, interface: str) -> dict[str, Any]: ...
    async def restart_networkd(self) -> dict[str, Any]: ...
    async def apply_interface_config(
        self, interface: str, ip_allocation: str,
        address: str | None, gateway: str | None, dns: list[str],
        vlan_children: list[str], atomic_updates: bool,
    ) -> dict[str, Any]: ...
    async def rollback_config(self, atomic_updates: bool) -> dict[str, Any]: ...
    async def persist_config(self, atomic_updates: bool) -> dict[str, Any]: ...

    # Routing and DNS
    async def get_routes(self) -> dict[str, Any]: ...
    async def get_dns(self) -> dict[str, Any]: ...
    async def test_connectivity(
        self, gateways: list[str], include_public: bool = True
    ) -> dict[str, Any]: ...

    # UPnP
    async def upnp_get_status(self) -> dict[str, Any]: ...
    async def upnp_get_mappings(self) -> dict[str, Any]: ...
    async def upnp_add_mapping(self, port: int) -> dict[str, Any]: ...
    async def upnp_remove_mapping(self, port: int) -> dict[str, Any]: ...

    # Traffic shaping
    async def get_shaping_policy(self) -> dict[str, Any]: ...
    async def set_shaping_policy(
        self, interface: str, rate: int | None
    ) -> dict[str, Any]: ...

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


NetworkFocus = Literal["tabs", "content", "buttons"]


class FocusableTabPane(TabPane):
    """Tab pane that can delegate focus to children."""

    def focus_child(self, last: bool = False) -> None:
        """Focus a child widget.

        Args:
            last: If True, focus the last focusable child
        """
        if not self.children:
            return

        first = self.children[0]

        if hasattr(first, "set_child_focus"):
            first.set_child_focus(first=not last)
        elif first.can_focus:
            first.focus()


class HorizontalButtons(Horizontal):
    """Horizontal container for action buttons."""

    def on_key(self, event: events.Key) -> None:
        """Handle left/right navigation between buttons."""
        if event.name not in ["left", "right"]:
            return

        event.stop()

        buttons = self.query(Button)
        for button in buttons:
            if not button.has_focus:
                button.focus()
                break


class NetworkScreen(Screen):
    """Network configuration screen.

    This screen allows users to:
    - View and configure network interfaces
    - View routing table and connectivity status
    - View DNS servers and name resolution
    - Manage UPnP port mappings
    - View network summary
    """

    class NextScreenRequested(Message):
        """Request to proceed to next screen."""

    class PreviousScreenRequested(Message):
        """Request to go back to previous screen."""

    class NetworkConfigSaved(Message):
        """Network configuration was saved."""

        def __init__(self, interface_address: str | None = None) -> None:
            super().__init__()
            self.interface_address = interface_address

    class NetworkComplete(Message):
        """Network configuration complete with all details."""

        def __init__(
            self,
            upnp_port_changed: bool,
            router_changed: bool,
            interface_ip_changed: bool,
            upnp_port: FluxApiPorts | None,
            router_address: str,
            interface_address: str,
        ) -> None:
            super().__init__()
            self.upnp_port_changed = upnp_port_changed
            self.router_changed = router_changed
            self.interface_ip_changed = interface_ip_changed
            self.upnp_port = upnp_port
            self.router_address = router_address
            self.interface_address = interface_address

    CSS_PATH = "../css/network_screen.tcss"

    BINDINGS = [
        Binding("down", "focus_content('down')", "Move down", show=False),
        Binding("up", "focus_content('up')", "Move up", show=False),
        Binding("escape", "confirm_exit", "Confirm Exit", show=False),
    ]

    network_focus: var[NetworkFocus] = var("tabs")

    def __init__(
        self,
        client: NetworkClient,
        show_header: bool = True,
        show_footer: bool = True,
        atomic_updates: bool = False,
        existing_upnp_port: FluxApiPorts | None = None,
        existing_router_address: str | None = None,
        existing_interface_address: str | None = None,
    ) -> None:
        """Initialize the network screen.

        Args:
            client: RPC client for network operations
            show_header: Whether to show the header
            show_footer: Whether to show the footer
            atomic_updates: Whether to apply changes atomically
            existing_upnp_port: Current UPnP port (for change detection)
            existing_router_address: Current router address
            existing_interface_address: Current interface address
        """
        super().__init__()
        self.title = "Network Configuration"
        self.client = client
        self.show_header = show_header
        self.show_footer = show_footer
        self.atomic_updates = atomic_updates
        self.existing_upnp_port = existing_upnp_port
        self.existing_router_address = existing_router_address
        self.existing_interface_address = existing_interface_address

        # Widgets
        self.interface_group = InterfaceGroup()
        self.routing = Routing(client)
        self.dns = Dns(client)
        self.upnp_builder = UpnpBuilder(client)
        self.network_summary = NetworkSummary()

        # State
        self.default_routes: list[Route] = []
        self.resolved_dns: list[ResolvedDnsServer] = []
        self.links: dict[int, str] = {}  # ifindex -> name
        self.interfaces_changed = False
        self.policy_changed = False

    def compose(self) -> ComposeResult:
        if self.show_header:
            yield Header()

        with TabbedContent(initial="interfaces"):
            with FocusableTabPane("Interfaces", id="interfaces"):
                yield self.interface_group
            with FocusableTabPane("Routing", id="routing"):
                yield self.routing
            with FocusableTabPane("DNS", id="dns"):
                yield self.dns
            with FocusableTabPane("UPnP", id="upnp"):
                yield self.upnp_builder
            with FocusableTabPane("Summary", id="summary"):
                yield self.network_summary

        with HorizontalButtons(id="network-bottom-bar"):
            yield Button("Cancel" if self.atomic_updates else "Back", id="network-back")
            yield Button("Next", id="network-next")

        if self.show_footer:
            yield Footer()

    async def on_mount(self) -> None:
        """Load initial data when mounted."""
        self._load_interfaces()
        self._load_routes()
        self._load_dns()
        self._load_upnp()

    @work(exclusive=True, group="load_interfaces")
    async def _load_interfaces(self) -> None:
        """Load network interfaces from backend."""
        try:
            result = await self.client.get_interfaces()

            if result.get("success"):
                interfaces = result.get("interfaces", [])
                self.links = result.get("links", {})

                for iface_data in interfaces:
                    iface = Interface.from_dict(iface_data)
                    iface.id = f"int-{len(self.interface_group.interfaces)}"
                    self.interface_group.interfaces.append(iface)

                if self.interface_group.is_mounted:
                    for iface in self.interface_group.interfaces:
                        await self.interface_group.mount(iface)

        except Exception as e:
            self.notify(f"Failed to load interfaces: {e}", severity="error")

    @work(exclusive=True, group="load_routes")
    async def _load_routes(self) -> None:
        """Load routing table from backend."""
        try:
            result = await self.client.get_routes()

            if result.get("success"):
                routes_data = result.get("routes", [])
                routes = [Route.from_dict(r) for r in routes_data]

                self.routing.update_routes(routes)

                # Store default routes
                self.default_routes = [r for r in routes if r.is_default]

                # Update summary
                if self.default_routes:
                    first_route = self.default_routes[0]
                    self.network_summary.set_default_route(f"via {first_route.gateway}")

        except Exception as e:
            self.notify(f"Failed to load routes: {e}", severity="error")

    @work(exclusive=True, group="load_dns")
    async def _load_dns(self) -> None:
        """Load DNS servers from backend."""
        try:
            result = await self.client.get_dns()

            if result.get("success"):
                servers_data = result.get("servers", [])
                self.links = result.get("links", self.links)

                servers = [ResolvedDnsServer.from_dict(s) for s in servers_data]
                self.resolved_dns = servers

                self.dns.update_servers(self.links, servers)

                # Update summary
                dns_str = ", ".join(set(s.address for s in servers))
                self.network_summary.set_dns(dns_str)

        except Exception as e:
            self.notify(f"Failed to load DNS: {e}", severity="error")

    @work(exclusive=True, group="load_upnp")
    async def _load_upnp(self) -> None:
        """Load UPnP status from backend."""
        try:
            result = await self.client.upnp_get_status()

            if result.get("success"):
                igd_available = result.get("igd_available", False)
                client_address = result.get("client_address")
                port_map_data = result.get("port_map", {})

                port_map = {
                    int(k): UpnpLease.from_dict(v) for k, v in port_map_data.items()
                }

                if igd_available and client_address:
                    local_addr = IPv4Address(client_address)
                    self.upnp_builder.set_selections(port_map, local_addr)

                    # Check for public IP
                    if local_addr.is_global:
                        self.upnp_builder.disable()
                        self.network_summary.set_network_address(str(local_addr))
                        self.network_summary.set_upnp_details("False", "None")
                    else:
                        self.network_summary.set_network_address(str(local_addr))
                else:
                    self.upnp_builder.set_igd_unavailable()

        except Exception as e:
            self.notify(f"Failed to load UPnP: {e}", severity="error")

    def on_button_pressed(self, event: Button.Pressed) -> None:
        """Handle button presses."""
        button_id = event.button.id

        tabs = self.get_child_by_type(TabbedContent)
        pane: FocusableTabPane = tabs.get_pane(tabs.active)

        self.network_focus = "buttons"

        if button_id == "network-next" and pane.id == "summary":
            self._persist_network_data()
        elif button_id == "network-next":
            self.action_move_tab(+1)
        elif button_id == "network-back" and pane.id == "interfaces":
            if self.atomic_updates:
                self.action_confirm_exit()
            else:
                self.post_message(self.PreviousScreenRequested())
        elif button_id == "network-back":
            self.action_move_tab(-1)

    def action_focus_content(self, direction: str) -> None:
        """Focus content based on direction."""
        tabs = self.get_child_by_type(TabbedContent)
        pane: FocusableTabPane = tabs.get_pane(tabs.active)

        if direction == "down" and self.network_focus == "tabs":
            self.network_focus = "content"
            pane.focus_child()
        elif direction == "up" and self.network_focus == "buttons":
            self.network_focus = "content"
            pane.focus_child(last=True)

    def action_confirm_exit(self) -> None:
        """Show exit confirmation dialog."""

        def handle_exit_action(exit_confirmed: bool | None) -> None:
            if exit_confirmed:
                self._rollback_changes()

        msg = "Are you sure? This will roll back any changes made."
        self.app.push_screen(
            ConfirmExitScreen(msg, action_label="Exit"), handle_exit_action
        )

    def action_move_tab(self, direction: int) -> None:
        """Move to next/previous tab."""
        tabbed_content = self.get_child_by_type(TabbedContent)
        pane = tabbed_content.get_pane(tabbed_content.active)

        for child in pane.children:
            if hasattr(child, "blur_children"):
                child.blur_children()

        tabs = self.query_one(Tabs)

        action = tabs.action_next_tab if direction > 0 else tabs.action_previous_tab

        action()

        self.network_focus = "tabs" if tabs.has_focus else "buttons"

    @work(exclusive=True, group="rollback")
    async def _rollback_changes(self) -> None:
        """Roll back changes and exit."""
        self.loading = True

        if self.policy_changed:
            try:
                await self.client.set_shaping_policy("", None)
            except Exception:
                pass

        if self.interfaces_changed:
            try:
                await self.client.rollback_config(self.atomic_updates)
            except Exception:
                pass

        self.loading = False
        self.post_message(self.PreviousScreenRequested())

    @work(exclusive=True, group="persist")
    async def _persist_network_data(self) -> None:
        """Save network configuration."""
        # Persist network configs to backup storage
        try:
            await self.client.persist_config(self.atomic_updates)
        except Exception:
            pass

        upnp_port = self.upnp_builder.selected_port

        interface_address = (
            self.network_summary.network_overview.network_address
            or self.existing_interface_address
            or ""
        )

        # Determine router address (prefer UPnP IGD)
        router_address = ""
        if self.upnp_builder.igd_available and self.upnp_builder.local_address:
            # Would need to get from backend - using default route for now
            pass

        if not router_address and self.default_routes:
            router_address = self.default_routes[0].gateway or ""

        upnp_changed = upnp_port != self.existing_upnp_port
        router_changed = router_address != self.existing_router_address
        interface_changed = interface_address != self.existing_interface_address

        msg = self.NetworkComplete(
            upnp_port_changed=upnp_changed,
            router_changed=router_changed,
            interface_ip_changed=interface_changed,
            upnp_port=upnp_port,
            router_address=router_address,
            interface_address=interface_address,
        )

        self.post_message(msg)
        self.post_message(self.NetworkConfigSaved(interface_address))

    @work(exclusive=True, group="apply_interface")
    async def _apply_interface_config(self, result: InterfaceConfigResult) -> None:
        """Apply interface configuration changes via RPC."""
        if result.change_type == "none" and not result.bandwidth_changed:
            return

        interface = result.interface

        try:
            if result.change_type == "add" and result.vlan:
                # Create VLAN subinterface
                vlan_result = await self.client.create_vlan(
                    interface.interface_name, int(result.vlan)
                )
                if not vlan_result.get("success"):
                    self.notify(
                        f"Failed to create VLAN: {vlan_result.get('error')}",
                        severity="error",
                    )
                    return

            elif result.change_type == "remove":
                # Delete VLAN interface
                delete_result = await self.client.delete_vlan(interface.interface_name)
                if not delete_result.get("success"):
                    self.notify(
                        f"Failed to delete VLAN: {delete_result.get('error')}",
                        severity="error",
                    )
                    return

            elif result.change_type == "change":
                # Get VLAN children for this interface
                vlan_children = self.interface_group.get_child_interface_names(interface)

                # Build address in CIDR format
                address: str | None = None
                if interface.ip_allocation == "static" and interface.address:
                    subnet = str(interface.address.network)
                    addr_str = str(interface.address.ip)
                    address = f"{addr_str}/{subnet.split('/')[1]}"

                gateway = interface.gateway if interface.gateway != "none" else None
                dns = interface.dns or []

                # Use the orchestrated RPC that handles service stop/start, UPnP, etc.
                config_result = await self.client.apply_interface_config(
                    interface=interface.interface_name,
                    ip_allocation=interface.ip_allocation,
                    address=address,
                    gateway=gateway,
                    dns=dns,
                    vlan_children=vlan_children,
                    atomic_updates=self.atomic_updates,
                )

                if not config_result.get("success"):
                    self.notify(
                        f"Failed to configure interface: {config_result.get('error', config_result.get('message'))}",
                        severity="error",
                    )
                    return

                self.interfaces_changed = True

            # Apply bandwidth policy
            if result.bandwidth_changed:
                policy_result = await self.client.set_shaping_policy(
                    interface.interface_name, result.bandwidth_limit
                )
                if policy_result.get("success"):
                    self.policy_changed = True

            # Reload data
            self._load_interfaces()
            self._load_routes()
            self._load_dns()

        except Exception as e:
            self.notify(f"Error applying configuration: {e}", severity="error")

    def _interface_configure_callback(
        self, result: InterfaceConfigResult | None
    ) -> None:
        """Handle interface modal result."""
        if result:
            self._apply_interface_config(result)

    @work(exclusive=True, group="upnp_reserve")
    async def _reserve_upnp_port(self, port: int) -> None:
        """Reserve a UPnP port mapping."""
        try:
            result = await self.client.upnp_add_mapping(port)

            if result.get("success"):
                host = result.get("host", "")
                remaining = result.get("remaining", 3600)
                self.upnp_builder.reserve_port_confirmed(port, host, remaining)
            else:
                self.upnp_builder.reserve_port_rejected(port)

        except Exception:
            self.upnp_builder.reserve_port_rejected(port)

    @work(exclusive=True, group="upnp_cancel")
    async def _cancel_upnp_port(self, port: int) -> None:
        """Cancel a UPnP port mapping."""
        try:
            result = await self.client.upnp_remove_mapping(port)

            if result.get("success"):
                self.upnp_builder.cancel_port_confirmed(port)
            else:
                self.upnp_builder.cancel_port_rejected(port)

        except Exception:
            self.upnp_builder.cancel_port_rejected(port)

    @on(TabbedContent.TabActivated)
    def on_tab_activated(self, event: TabbedContent.TabActivated) -> None:
        """Update button labels when tabs change."""
        tab_id = event.tab.id

        previous_text = "Cancel" if self.atomic_updates else "Back"

        next_label = "Confirm" if tab_id == "--content-tab-summary" else "Next"
        previous_label = (
            previous_text if tab_id == "--content-tab-interfaces" else "Back"
        )

        self.query_one("#network-next", Button).label = next_label
        self.query_one("#network-back", Button).label = previous_label

    @on(FocusTabsRequested)
    def on_focus_tabs_requested(self, event: FocusTabsRequested) -> None:
        """Focus the tabs."""
        self.network_focus = "tabs"
        tabs = self.query_one(Tabs)
        tabs.focus()

    @on(FocusButtonsRequested)
    def on_focus_buttons_requested(self, event: FocusButtonsRequested) -> None:
        """Focus the buttons."""
        self.network_focus = "buttons"
        next_btn = self.query_one("#network-next", Button)
        back_btn = self.query_one("#network-back", Button)

        if not next_btn.disabled:
            next_btn.focus()
        else:
            back_btn.focus()

    @on(InterfaceGroup.InterfaceModalRequested)
    def on_interface_modal_requested(
        self, event: InterfaceGroup.InterfaceModalRequested
    ) -> None:
        """Open the interface configuration modal."""
        interface = event.interface

        # Get default gateway for this interface
        gateway = ""
        for route in self.default_routes:
            if route.link == interface.interface_name:
                gateway = route.gateway or ""
                break

        # Get DNS servers
        dns_servers = list(set(s.address for s in self.resolved_dns))

        # Get shaping policy (would need to call RPC)
        shaping_policy = InterfaceShapingPolicy()

        self.app.push_screen(
            InterfaceModalScreen(
                interface,
                gateway,
                dns_servers,
                shaping_policy,
            ),
            self._interface_configure_callback,
        )

    @on(UpnpBuilder.ApiPortSelected)
    def on_upnp_port_selected(self, event: UpnpBuilder.ApiPortSelected) -> None:
        """Update summary when UPnP port is selected."""
        enabled = str(event.port is not None)
        port = str(event.port) if event.port else "None"

        self.network_summary.set_upnp_details(enabled, port)

    @on(UpnpBuilder.ReservePortRequested)
    def on_upnp_reserve_requested(
        self, event: UpnpBuilder.ReservePortRequested
    ) -> None:
        """Reserve a UPnP port."""
        if event.port:
            self._reserve_upnp_port(event.port)

    @on(UpnpBuilder.CancelPortRequested)
    def on_upnp_cancel_requested(self, event: UpnpBuilder.CancelPortRequested) -> None:
        """Cancel a UPnP port reservation."""
        if event.port:
            self._cancel_upnp_port(event.port)

    @on(Connectivity.ConnectivityStateChanged)
    def on_connectivity_changed(
        self, event: Connectivity.ConnectivityStateChanged
    ) -> None:
        """Update summary when connectivity changes."""
        self.network_summary.set_connectivity(event.state)

        # On up transition, refresh UPnP
        if event.edge_transition:
            self._load_upnp()
