"""UPnP port selection widget."""

from __future__ import annotations

from ipaddress import IPv4Address
from typing import Any, Protocol

from rich.text import Text
from textual import events, on
from textual.app import ComposeResult
from textual.dom import NoMatches
from textual.message import Message
from textual.reactive import var
from textual.widget import Widget
from textual.widgets import Label, SelectionList
from textual.widgets.selection_list import Selection

from flux_networking_shared.tui.messages import FocusTabsRequested
from flux_networking_shared.tui.models.upnp import (
    FLUX_API_PORTS,
    FluxApiPorts,
    UpnpLease,
)
from flux_networking_shared.tui.widgets.focus_label import FocusLabel
from flux_networking_shared.tui.widgets.interface_group import FocusButtonsRequested
from flux_networking_shared.tui.widgets.upnp_action_bar import UpnpActionBar


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

    async def upnp_get_status(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]: ...


class UpnpBuilder(Widget, can_focus=True):
    """Widget for selecting and reserving UPnP port mappings.

    Displays available Flux API ports (16127-16197) and their current
    lease status. Users can select a port and reserve it via UPnP.
    """

    class ReservePortRequested(Message):
        """Posted when user requests to reserve a port."""

        def __init__(self, port: FluxApiPorts | None) -> None:
            super().__init__()
            self.port = port

    class CancelPortRequested(Message):
        """Posted when user requests to cancel a port reservation."""

        def __init__(self, port: FluxApiPorts | None) -> None:
            super().__init__()
            self.port = port

    class ApiPortSelected(Message):
        """Posted when a port is selected or deselected."""

        def __init__(self, port: FluxApiPorts | None) -> None:
            super().__init__()
            self.port = port

    class NetworkAddressRequested(Message):
        """Posted when network address information is needed."""

    BORDER_TITLE = "Select an API Port"

    DEFAULT_CSS = """
        UpnpBuilder {
            height: auto;
            border: solid $primary;
            padding: 1;
        }

        UpnpBuilder #upnp-header {
            text-style: bold;
            margin-bottom: 1;
        }

        UpnpBuilder SelectionList {
            height: auto;
            max-height: 12;
        }

        UpnpBuilder #upnp-info-label {
            margin-top: 1;
            text-align: center;
        }

        UpnpBuilder #upnp-info-label.hidden {
            display: none;
        }

        UpnpBuilder #public-ip-overlay {
            width: 100%;
            height: auto;
            content-align: center middle;
        }
    """

    override_enabled = var(False)
    selected_port: var[FluxApiPorts | None] = var(None)
    port_reserved = var(False)
    igd_available = var(False)
    local_address: var[IPv4Address | None] = var(None)
    focused_area: var[str | None] = var(None)

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

        Args:
            client: RPC client for UPnP operations
        """
        super().__init__()

        self.client = client
        self.selections: list[Selection[FluxApiPorts]] = []
        self.host_to_port_map: dict[str, FluxApiPorts] = {}

        self.upnp_action_bar = UpnpActionBar()
        self.selection_list = SelectionList(*self.selections, id="upnp-selection-list")

    @staticmethod
    def build_prompt(
        port: int, host: str | None = None, lease: int | None = None
    ) -> str:
        """Build a formatted prompt string for the selection list.

        Args:
            port: The port number
            host: Optional host IP that has the lease
            lease: Optional remaining lease time in seconds

        Returns:
            Formatted prompt string
        """
        if not host:
            return str(port)
        else:
            formatted_port = f"{str(port).ljust(7)}   "
            host_str = f"{host.ljust(17)}   "
            formatted_lease = "" if lease is None else str(lease)

            return f"{formatted_port}{host_str}{formatted_lease}"

    @staticmethod
    def has_lease(selection: Selection) -> bool:
        """Check if a selection has an active lease.

        Args:
            selection: The selection to check

        Returns:
            True if the selection has lease info in its prompt
        """
        return len(str(selection.prompt)) > 5

    @property
    def selections_available(self) -> bool:
        """Check if any selections are available (not disabled)."""
        return any(not x.disabled for x in self.selections)

    @property
    def selected(self) -> Selection | None:
        """Get the currently selected Selection object."""
        return next(
            filter(
                lambda x: x.value == self.selected_port,
                self.selections,
            ),
            None,
        )

    @property
    def selected_index(self) -> int | None:
        """Get the index of the currently selected item."""
        try:
            index = self.selections.index(self.selected)
        except (ValueError, TypeError):
            index = None

        return index

    @property
    def available_option_indexes(self) -> list[int]:
        """Get indexes of all available (non-disabled) options."""
        return [i for i, x in enumerate(self.selections) if not x.disabled]

    @property
    def global_address(self) -> bool:
        """Check if the local address is a global/public IP."""
        if not self.local_address:
            return False

        return self.local_address.is_global

    def compose(self) -> ComposeResult:
        port = "Port".ljust(7)
        host = "Host".ljust(17)
        header = f"     {port}   {host}   Remaining Sec"

        info = (
            ""
            if self.igd_available
            else Text(
                "Unable to connect to UPnP router",
                style="bold red",
            )
        )
        info_classes = "hidden" if self.igd_available else ""

        self.build_selections({})
        self.selection_list.add_options(self.selections)

        yield Label(header, id="upnp-header")
        yield self.selection_list
        yield self.upnp_action_bar
        yield Label(info, id="upnp-info-label", classes=info_classes)

    def build_selections(self, port_data: dict[int, UpnpLease]) -> None:
        """Build the selection list from port mapping data.

        Args:
            port_data: Mapping of port numbers to their lease info
        """
        self.selection_list.clear_options()
        self.selections = []

        for port in FLUX_API_PORTS:
            if self.igd_available:
                lease = port_data.get(port, None)

                host = lease.host if lease else None
                remaining = lease.remaining_s if lease else None
                disabled = bool(host)
            else:
                host = "Unknown"
                disabled = True
                remaining = None

            prompt = self.build_prompt(port, host, remaining)
            self.selections.append(
                Selection(prompt, port, disabled=disabled, id=str(port))
            )

    def set_child_focus(self, first: bool = True) -> None:
        """Focus a child widget.

        Args:
            first: If True, focus first focusable child; otherwise focus last
        """

        def find_first_focusable(widget: Widget, rev: bool) -> Widget | None:
            children = widget.children[::-1] if rev else widget.children

            for child in children:
                if child.can_focus and not child.disabled:
                    return child

                if focusable := find_first_focusable(child, rev):
                    return focusable

            return None

        focusable = find_first_focusable(self, not first)

        if focusable:
            focusable.focus()

    def get_selection_by_value(self, value: FluxApiPorts) -> Selection | None:
        """Get a selection by its port value.

        Args:
            value: The port number to find

        Returns:
            The Selection object or None
        """
        return next(filter(lambda x: x.value == value, self.selections), None)

    def set_igd_unavailable(self) -> None:
        """Mark IGD as unavailable and reset selections."""
        self.igd_available = False
        self.set_selections({})

    def disable(self) -> None:
        """Disable the widget (for public IP scenarios)."""
        self.selections.clear()
        self.selected_port = None
        self.remove_children()
        self.border_title = None
        self.mount(
            FocusLabel(
                "This node has a public IP, UPnP not necessary",
                id="public-ip-overlay",
            )
        )

    def set_selections(
        self,
        port_map: dict[int, UpnpLease] | None = None,
        local_address: IPv4Address | None = None,
    ) -> None:
        """Update selections with new port mapping data.

        Args:
            port_map: Mapping of ports to lease info
            local_address: The local IP address of this client
        """
        if self.global_address:
            return

        self.local_address = local_address

        if local_address:
            self.igd_available = True

        port_map = port_map or {}

        self.host_to_port_map = {v.host: k for k, v in port_map.items()}

        if self.selected_port:
            self.post_message(self.ApiPortSelected(None))

        self.selected_port = None
        self.port_reserved = False

        self.build_selections(port_map)

        self.upnp_action_bar.cancel_disabled = True
        self.upnp_action_bar.update_switch_prevent(False)

        self.set_info_message(clear=True)

        self.selection_list.add_options(self.selections)
        self.set_selection_if_host_available()
        self.selection_list.refresh()

    def set_selection_if_host_available(self) -> None:
        """Auto-select port if this host already has a lease."""
        current_port = self.host_to_port_map.get(str(self.local_address), None)

        if not current_port:
            return

        self.selected_port = current_port

        self.post_message(self.ApiPortSelected(current_port))

        selection = self.get_selection_by_value(current_port)
        if selection:
            selection.disabled = False

            self.selection_list.select(selection)
            index = self.selections.index(selection)
            self.selection_list.highlighted = index

        self.upnp_action_bar.reserve_disabled = False

        if not self.selections_available:
            self.set_info_message(
                clear=False,
                msg="Warning! All Flux UPnP Api Ports on this network are used (or unknown). Beware.",
            )

    def set_info_message(
        self,
        clear: bool = False,
        msg: str | None = None,
        style: str = "bold red",
    ) -> None:
        """Set or clear the info message label.

        Args:
            clear: If True, clear the message
            msg: Message to display
            style: Rich text style for the message
        """
        try:
            info_label = self.query_one("#upnp-info-label", Label)
        except NoMatches:
            return

        if clear:
            info_label.update("")
            info_label.set_class(True, "hidden")
            return

        if msg:
            info_label.update(
                Text(
                    msg,
                    style=style,
                )
            )
            info_label.set_class(False, "hidden")

    def disable_used_selections(self) -> None:
        """Disable selections that are in use by other hosts."""
        existing_port = self.host_to_port_map.get(str(self.local_address))

        for selection in self.selections:
            if (
                self.has_lease(selection)
                and existing_port
                and existing_port != selection.value
            ):
                selection.disabled = True

    def reserve_port_confirmed(
        self, port: FluxApiPorts, host: str, remaining: int
    ) -> None:
        """Handle successful port reservation.

        Args:
            port: The reserved port
            host: The host IP
            remaining: Remaining lease time in seconds
        """
        self.port_reserved = True
        self.upnp_action_bar.cancel_disabled = False

        selection = self.get_selection_by_value(self.selected_port)

        if selection:
            prompt = self.build_prompt(port, host, remaining)
            self.selection_list.replace_option_prompt(selection.id, prompt)

        self.upnp_action_bar.focus_button("reserve-port")

        self.set_info_message(
            msg=f"Port: {port} has been reserved for 1hr. Proceed",
            style="bold green",
        )

    def reserve_port_rejected(self, port: int) -> None:
        """Handle failed port reservation.

        Args:
            port: The port that failed to reserve
        """
        msg = f"Unable to reserve port: {port}. Is it in use?"
        self.set_info_message(msg=msg)

    def cancel_port_confirmed(self, port: int) -> None:
        """Handle successful port cancellation.

        Args:
            port: The cancelled port
        """
        self.port_reserved = False
        self.upnp_action_bar.cancel_disabled = True

        selection = self.get_selection_by_value(port)

        if not selection:
            return

        prompt = self.build_prompt(port)
        self.selection_list.replace_option_prompt(selection.id, prompt)
        self.selection_list.focus()

        self.set_info_message(clear=True)

    def cancel_port_rejected(self, port: int) -> None:
        """Handle failed port cancellation.

        Args:
            port: The port that failed to cancel
        """
        msg = f"Unable to cancel port: {port}"
        self.set_info_message(msg=msg)

    def on_focus(self) -> None:
        """Handle focus events."""
        if self.focused_area == "upnp-selection-list":
            self.focused_area = None
            self.post_message(FocusTabsRequested())
            return

        if self.global_address:
            try:
                self.query_one(FocusLabel).focus()
            except NoMatches:
                pass
        else:
            try:
                self.query_one(SelectionList).focus()
            except NoMatches:
                pass

    def on_descendant_focus(self, event: events.DescendantFocus) -> None:
        """Track which area has focus."""
        if event.widget.id == "upnp-selection-list":
            widget: SelectionList = event.widget

            if widget.highlighted is None:
                available = self.available_option_indexes
                widget.highlighted = available[0] if available else None

            self.focused_area = "upnp-selection-list"
        else:
            self.focused_area = "upnp-action-bar"

    @on(UpnpActionBar.OverrideChanged)
    def on_override_changed(self, event: UpnpActionBar.OverrideChanged) -> None:
        """Handle override switch changes."""
        self.override_enabled = event.override_enabled

    @on(UpnpActionBar.ReservePressed)
    def on_port_reserved(self, event: UpnpActionBar.ReservePressed) -> None:
        """Handle reserve button press."""
        self.post_message(self.ReservePortRequested(self.selected_port))

    @on(UpnpActionBar.CancelPressed)
    def on_port_canceled(self, event: UpnpActionBar.CancelPressed) -> None:
        """Handle cancel button press."""
        port = self.selected_port
        self.selected_port = None

        self.selection_list.deselect_all()

        self.post_message(self.CancelPortRequested(port))

    def on_selection_list_selection_toggled(
        self, event: SelectionList.SelectionToggled
    ) -> None:
        """Handle selection changes in the port list."""
        event.stop()
        selection_list = event.selection_list

        if not selection_list.selected:
            self.upnp_action_bar.reserve_disabled = True

        if self.port_reserved:
            with self.prevent(SelectionList.SelectionToggled):
                selection_list.toggle(event.selection)

            msg = "Cancel the current port before requesting another"
            self.set_info_message(msg=msg)

            return

        self.set_info_message(clear=True)

        port = event.selection.value

        port = port if port in selection_list.selected else None

        self.selected_port = port

        for selection in selection_list.selected:
            if selection == port:
                continue

            with self.prevent(SelectionList.SelectionToggled):
                selection_list.toggle(selection)

        existing_port = self.host_to_port_map.get(str(self.local_address))
        this_port = existing_port and existing_port == port

        if self.has_lease(event.selection) and port and not this_port:
            self.set_info_message(
                False,
                "Warning! You are selecting a upnp port that is currently in use "
                "by another Node. This node will not start while an existing mapping is present.",
            )

        self.post_message(self.ApiPortSelected(port))

    def on_key(self, event: events.Key) -> None:
        """Handle keyboard navigation."""
        event_name = event.name

        if event_name not in ["up", "down", "shift_tab"]:
            return

        highlighted = self.selection_list.highlighted
        available = self.available_option_indexes
        lowest_available = available[0] if available else None
        highest_available = available[-1] if available else None

        msg: FocusTabsRequested | FocusButtonsRequested | None = None
        handled_internally = False

        if (
            self.focused_area == "upnp-selection-list"
            and highlighted == highest_available
            and event_name == "down"
        ):
            try:
                self.query_one(UpnpActionBar).focus()
            except NoMatches:
                pass
            handled_internally = True
        elif self.focused_area == "upnp-action-bar" and event_name == "up":
            try:
                self.query_one(SelectionList).focus()
            except NoMatches:
                pass
            handled_internally = True
        elif self.focused_area == "upnp-action-bar" and event_name == "shift_tab":
            try:
                self.query_one(SelectionList).focus()
            except NoMatches:
                pass
            handled_internally = True

        if handled_internally:
            event.stop()
            return

        if (
            self.focused_area == "upnp-selection-list"
            and highlighted == lowest_available
            and event_name == "up"
        ):
            self.focused_area = None
            msg = FocusTabsRequested()

        elif self.focused_area == "upnp-action-bar" and event_name == "down":
            self.focused_area = None
            msg = FocusButtonsRequested()

        if msg:
            event.stop()
            self.post_message(msg)

    # WATCHERS

    def watch_override_enabled(self, old: bool, new: bool) -> None:
        """Handle override mode changes."""
        if old == new:
            return

        if new:
            for selection in self.selections:
                selection.disabled = False

            self.selection_list.refresh()
            return

        if not self.igd_available:
            self.selection_list.deselect_all()
            self.selected_port = None
            self.upnp_action_bar.reserve_disabled = True

        self.selection_list.highlighted = None

        highlight_required = False
        prior_index = None

        self.disable_used_selections()

        if self.selected:
            self.set_info_message(clear=True)
            self.post_message(self.ApiPortSelected(None))
            highlight_required = True
            prior_index = self.selected_index
            self.selection_list.deselect(self.selected)
            self.selected_port = None
            self.upnp_action_bar.cancel_disabled = True

        if highlight_required:
            if available := self.available_option_indexes:
                if prior_index in available:
                    self.selection_list.highlighted = prior_index
                    return

                next_available = max(x for x in available)
                self.selection_list.highlighted = next_available

        self.selection_list.refresh()

    def watch_selected_port(self, value: FluxApiPorts | None) -> None:
        """Update reserve button state based on selection."""
        self.upnp_action_bar.reserve_disabled = not value

    def watch_igd_available(self, old: bool, new: bool) -> None:
        """Update info message based on IGD availability."""
        if old == new:
            return

        if new:
            self.set_info_message(True)
        else:
            self.set_info_message(False, "Unable to connect to upnp router")
