"""Main TUI application for porTUI."""

from __future__ import annotations

from textual.app import App, ComposeResult
from textual.containers import Horizontal
from textual.reactive import reactive
from textual.timer import Timer
from textual.widgets import DataTable, Footer, Header, Input, Label
from textual.widgets._data_table import RowDoesNotExist
from textual.worker import get_current_worker

from portui.collector import PortDataCollector
from portui.config import ConfigManager
from portui.models import PortInfo
from portui.screens import (
    ColumnConfigScreen,
    HelpScreen,
    IntervalScreen,
    KillDialogScreen,
    PortDetailScreen,
)
from portui.sort import SortController
from portui.tree import ProcessTreeBuilder

# ---------------------------------------------------------------------------
# Row-key helpers — single source of truth for key format used by
# _apply_filter (writer) and _get_selected_port (reader).
# ---------------------------------------------------------------------------
_ROW_KEY_SEP = "-"


def _make_row_key(port: PortInfo) -> str:
    return f"{port.pid}{_ROW_KEY_SEP}{port.port}{_ROW_KEY_SEP}{port.protocol}{_ROW_KEY_SEP}{port.ip}"


def _parse_row_key(key_str: str) -> tuple[int, int, str, str] | None:
    """Return (pid, port, protocol, ip) from a row key string, or None on failure."""
    parts = key_str.split(_ROW_KEY_SEP, 3)
    if len(parts) < 4:
        return None
    try:
        return int(parts[0]), int(parts[1]), parts[2], parts[3]
    except ValueError:
        return None


class PortUIApp(App):
    """Main TUI application for port monitoring."""

    TITLE = "porTUI"

    CSS = """
    Screen {
        align: center middle;
    }

    #toolbar {
        display: none;
        height: auto;
        background: $surface;
        padding: 0 1;
        color: $text;
    }

    #toolbar.filter-active {
        display: block;
    }

    #filter-input {
        width: 1fr;
        background: $surface-darken-1;
        border: solid $warning;
        margin: 0 0 0 1;
    }

    #filter-label {
        padding: 1 0 0 0;
    }

    #filter-mode-indicator {
        display: none;
        color: $text;
        background: $warning;
        text-style: bold;
        padding: 1 1 0 0;
    }

    #toolbar.filter-active #filter-mode-indicator {
        display: block;
    }

    #port-table {
        height: 1fr;
        border: solid $primary;
    }

    #port-table:focus {
        border: solid $accent;
    }

    #port-table > .datatable--cursor {
        background: $accent 50%;
    }

    #status-bar {
        height: 1;
        background: $surface-darken-1;
        color: $text;
        padding: 0 1;
        text-style: dim;
    }

    #status-bar.paused {
        background: $warning;
        color: $text;
        text-style: bold;
    }

    #detail-container {
        width: 80%;
        max-width: 100;
        height: auto;
        border: solid $accent;
        background: $surface;
        padding: 1 2;
    }

    #detail-container Label {
        margin: 0;
    }

    #detail-container #title {
        text-style: bold;
        color: $accent;
        margin: 0 0 1 0;
    }

    #detail-container #detail-command {
        width: 100%;
    }

    #detail-container #detail-hint {
        text-style: dim;
    }

    #dialog {
        width: 60;
        height: auto;
        border: solid $primary;
        background: $surface;
        padding: 1 2;
    }

    #dialog > Label {
        margin: 0 0 1 0;
    }

    #dialog #title {
        text-style: bold;
        color: $warning;
    }

    #help-container {
        width: 80;
        height: 30;
        border: solid $primary;
        background: $surface;
        padding: 1 2;
    }

    #help-container MarkdownViewer {
        height: 1fr;
    }

    #help-container Button {
        margin: 1 0 0 0;
        width: 100%;
    }

    Checkbox {
        margin: 0 0 1 0;
    }

    Horizontal Button {
        margin: 0 1 0 0;
    }
    """

    BINDINGS = [
        ("q", "quit", "Quit"),
        ("r", "refresh", "Refresh"),
        ("p", "toggle_pause", "Pause/Resume"),
        ("slash", "focus_filter", "Filter"),
        ("c", "toggle_columns", "Columns"),
        ("i", "set_interval", "Interval"),
        ("x", "kill_selected", "Kill"),
        ("s", "cycle_sort", "Sort"),
        ("h", "cycle_protocol", "Proto"),
        ("t", "show_tree", "Tree"),
        ("?", "show_help", "Help"),
        ("j", "cursor_down", "Down"),
        ("k", "cursor_up", "Up"),
        ("up", "cursor_up", "Up"),
        ("down", "cursor_down", "Down"),
    ]

    COLUMN_NAMES = ["Port", "IP", "Proto", "Process", "PID", "User", "State", "Command"]
    COLUMN_KEYS = ["port", "ip", "proto", "process", "pid", "user", "state", "cmdline"]

    ports: reactive[list[PortInfo]] = reactive([])
    auto_refresh: reactive[bool] = reactive(True)
    refresh_interval: reactive[int] = reactive(5)
    is_refreshing: reactive[bool] = reactive(False)
    filter_text: reactive[str] = reactive("")
    user_interacting: reactive[bool] = reactive(False)
    visible_columns: reactive[tuple[bool, ...]] = reactive((True,) * 8)

    _refresh_timer: Timer | None = None
    _tree_mode: bool = False
    _protocol_filter: str = "both"  # "both" | "tcp" | "udp"

    def __init__(self, **kwargs) -> None:
        super().__init__(**kwargs)
        self._config = ConfigManager()
        self._sorter = SortController()

    # ------------------------------------------------------------------
    # Lifecycle
    # ------------------------------------------------------------------

    def compose(self) -> ComposeResult:
        """Compose the UI."""
        yield Header(show_clock=True)
        yield DataTable(id="port-table")
        with Horizontal(id="toolbar"):
            yield Label("Filter: ", id="filter-label")
            yield Label(" FILTER MODE ", id="filter-mode-indicator")
            yield Input(id="filter-input", placeholder="press / to filter...")
        yield Label(id="status-bar")
        yield Footer()

    def on_mount(self) -> None:
        """Initialize on mount."""
        cfg = self._config.load()
        self.refresh_interval = cfg["refresh_interval"]
        self.visible_columns = cfg["visible_columns"]
        self.auto_refresh = cfg["auto_refresh"]

        table = self.query_one("#port-table", DataTable)
        self._update_table_columns()
        table.cursor_type = "row"
        table.focus()
        self._start_refresh_timer()
        self.action_refresh()

    def on_unmount(self) -> None:
        """Save config on exit."""
        self._config.save(
            self.refresh_interval, self.visible_columns, self.auto_refresh
        )

    # ------------------------------------------------------------------
    # Key handling
    # ------------------------------------------------------------------

    def on_key(self, event) -> None:
        """Handle filter open/close keys."""
        filter_input = self.query_one("#filter-input", Input)
        if event.key == "slash" and not filter_input.has_focus:
            filter_input.focus()
            self.user_interacting = True
            event.stop()
        elif event.key == "escape" and filter_input.has_focus:
            filter_input.value = ""
            self.filter_text = ""
            self._apply_filter()
            self._update_status()
            self.query_one("#toolbar").remove_class("filter-active")
            self.query_one("#port-table", DataTable).focus()
            self.user_interacting = False
            event.stop()

    # ------------------------------------------------------------------
    # Reactive watchers
    # ------------------------------------------------------------------

    def watch_ports(self, _: list[PortInfo]) -> None:
        """Update table when port data changes."""
        self._apply_filter()
        self._update_status()

    def watch_auto_refresh(self, _: bool) -> None:
        """Update status bar when auto-refresh toggle changes."""
        self._update_status()

    def watch_visible_columns(self, _: tuple) -> None:
        """Rebuild columns and re-render table when visibility changes."""
        self._update_table_columns()
        self._apply_filter()

    # ------------------------------------------------------------------
    # Timer / refresh
    # ------------------------------------------------------------------

    def _start_refresh_timer(self) -> None:
        if self._refresh_timer:
            self._refresh_timer.stop()
        self._refresh_timer = self.set_interval(
            self.refresh_interval, self._scheduled_refresh
        )

    def _scheduled_refresh(self) -> None:
        if self.auto_refresh and not self.is_refreshing and not self.user_interacting:
            self.action_refresh()

    def action_refresh(self) -> None:
        """Refresh the port list."""
        if self.is_refreshing:
            return
        self.is_refreshing = True
        self.run_worker(self._fetch_ports, thread=True)

    def _fetch_ports(self) -> None:
        """Worker thread: collect port data and post to main thread."""
        worker = get_current_worker()
        if worker.is_cancelled:
            return
        try:
            ports = PortDataCollector.get_listening_ports()
        except Exception:
            ports = []
        self.call_from_thread(self._update_ports, ports)

    def _update_ports(self, ports: list[PortInfo]) -> None:
        """Main-thread callback: store new port data."""
        self.ports = self._sorter.apply(ports)
        self.is_refreshing = False

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

    def action_toggle_pause(self) -> None:
        """Toggle auto-refresh pause state."""
        self.auto_refresh = not self.auto_refresh

    def action_cursor_down(self) -> None:
        table = self.query_one("#port-table", DataTable)
        if table.cursor_row is not None and table.cursor_row < table.row_count - 1:
            table.cursor_coordinate = (table.cursor_row + 1, 0)

    def action_cursor_up(self) -> None:
        table = self.query_one("#port-table", DataTable)
        if table.cursor_row is not None and table.cursor_row > 0:
            table.cursor_coordinate = (table.cursor_row - 1, 0)

    def action_cycle_sort(self) -> None:
        """Advance to the next sort column."""
        self._sorter.cycle()
        self.ports = self._sorter.apply(self.ports)
        self._apply_filter()
        self._update_status()

    def action_focus_filter(self) -> None:
        """Focus the filter input."""
        self.query_one("#filter-input", Input).focus()
        self.user_interacting = True
        self.query_one("#toolbar").add_class("filter-active")

    def action_toggle_columns(self) -> None:
        """Open column visibility configuration."""
        self.push_screen(
            ColumnConfigScreen(self.visible_columns, self.COLUMN_NAMES),
            self._on_columns_set,
        )

    def _on_columns_set(self, result: list[bool] | None) -> None:
        if result is not None:
            self.visible_columns = tuple(result)

    def action_set_interval(self) -> None:
        """Open refresh interval picker."""
        self.push_screen(IntervalScreen(self.refresh_interval), self._on_interval_set)

    def _on_interval_set(self, result: int | None) -> None:
        if result is not None:
            self.refresh_interval = result
            self._start_refresh_timer()
            self._update_status()

    def action_kill_selected(self) -> None:
        """Open kill confirmation dialog for the selected process."""
        port = self._get_selected_port()
        if port is None:
            self.notify("No process selected", severity="warning")
            return
        self.push_screen(KillDialogScreen(port), self._on_kill_result)

    def _on_kill_result(self, result: tuple[PortInfo, bool] | None) -> None:
        if result is None:
            return
        port, force = result
        success, message = PortDataCollector.kill_process(
            port.pid, force, expected_create_time=port.create_time
        )
        if success:
            self.notify(message, severity="information")
            self.action_refresh()
        else:
            self.notify(message, severity="error")

    def action_show_help(self) -> None:
        self.push_screen(HelpScreen())

    def action_cycle_protocol(self) -> None:
        """Cycle protocol filter: both → tcp → udp → both."""
        cycle = {"both": "tcp", "tcp": "udp", "udp": "both"}
        self._protocol_filter = cycle[self._protocol_filter]
        self._apply_filter()
        self._update_status()

    def action_show_tree(self) -> None:
        """Toggle htop-style inline tree view."""
        self._tree_mode = not self._tree_mode
        self._apply_filter()
        self._update_status()

    # ------------------------------------------------------------------
    # Input events
    # ------------------------------------------------------------------

    def on_input_changed(self, event: Input.Changed) -> None:
        if event.input.id == "filter-input":
            self.filter_text = event.value
            self._apply_filter()
            self._update_status()

    def on_input_blur(self, event: Input.Blur) -> None:
        if event.input.id == "filter-input":
            self.user_interacting = False
            self.query_one("#toolbar").remove_class("filter-active")

    def on_input_focus(self, event: Input.Focus) -> None:
        if event.input.id == "filter-input":
            self.user_interacting = True
            self.query_one("#toolbar").add_class("filter-active")

    def on_data_table_row_selected(self, event: DataTable.RowSelected) -> None:
        """Show full process details on Enter."""
        port = self._get_selected_port()
        if port is not None:
            self.push_screen(PortDetailScreen(port))

    # ------------------------------------------------------------------
    # Internal helpers
    # ------------------------------------------------------------------

    def _update_table_columns(self) -> None:
        table = self.query_one("#port-table", DataTable)
        table.clear(columns=True)
        for name, visible in zip(self.COLUMN_NAMES, self.visible_columns):
            if visible:
                table.add_column(name)

    def _update_status(self) -> None:
        status = self.query_one("#status-bar", Label)
        filter_info = f" [filter: {self.filter_text}]" if self.filter_text else ""
        sort_info = (
            f" [sort: {self._sorter.column} {'↑' if self._sorter.ascending else '↓'}]"
            if self._sorter.column
            else ""
        )
        proto_info = (
            f" [{self._protocol_filter.upper()}]"
            if self._protocol_filter != "both"
            else ""
        )
        tree_info = " [TREE]" if self._tree_mode else ""
        if self.auto_refresh:
            refresh_status = f"{self.refresh_interval}s [ON]"
            status.remove_class("paused")
        else:
            refresh_status = "PAUSED — press p to resume"
            status.add_class("paused")
        status_text = (
            f"{len(self.ports)} ports{filter_info}{sort_info}{proto_info}{tree_info}"
            f" • {refresh_status}"
            f" • jk nav • / filter • h proto • x kill • t tree • s sort • q quit"
        )
        status.update(status_text)

    def _matches_filter(self, port: PortInfo) -> bool:
        """True if *port* passes the current protocol and text filter."""
        if self._protocol_filter == "tcp" and port.protocol != "TCP":
            return False
        if self._protocol_filter == "udp" and port.protocol != "UDP":
            return False
        if not self.filter_text:
            return True
        fl = self.filter_text.lower()
        return (
            fl in str(port.port)
            or fl in port.ip.lower()
            or fl in port.protocol.lower()
            or fl in port.process_name.lower()
            or fl in str(port.pid)
            or fl in port.username.lower()
            or fl in port.state.lower()
            or fl in port.cmdline.lower()
        )

    def _get_filtered_ports(self) -> list[PortInfo]:
        return [p for p in self.ports if self._matches_filter(p)]

    def _get_selected_port(self) -> PortInfo | None:
        """Return the PortInfo for the currently highlighted row, or None."""
        table = self.query_one("#port-table", DataTable)
        if table.cursor_row is None or table.row_count == 0:
            return None
        try:
            row_key, _ = table.coordinate_to_cell_key(table.cursor_coordinate)
        except (IndexError, KeyError):
            return None

        key_str = str(row_key.value) if hasattr(row_key, "value") else str(row_key)
        if key_str.startswith("ancestor-"):
            return None

        parsed = _parse_row_key(key_str)
        if parsed is None:
            return None
        target_pid, target_port, target_proto, target_ip = parsed

        for p in self.ports:
            if (
                p.pid == target_pid
                and p.port == target_port
                and p.protocol == target_proto
                and p.ip == target_ip
            ):
                return p
        return None

    def _apply_filter(self) -> None:
        """Rebuild the DataTable rows, preserving the cursor position."""
        table = self.query_one("#port-table", DataTable)

        # Save current selection
        saved_key = None
        if table.cursor_row is not None and table.row_count > 0:
            try:
                saved_key, _ = table.coordinate_to_cell_key(table.cursor_coordinate)
            except (IndexError, KeyError):
                pass

        table.clear()
        filtered = [p for p in self.ports if self._matches_filter(p)]

        if self._tree_mode:
            builder = ProcessTreeBuilder(self._sorter.column, self._sorter.ascending)
            tree_rows = builder.build(filtered)
            for port, ancestor_pid, ancestor_name, prefix in tree_rows:
                if port is not None:
                    row_data = [
                        str(port.port),
                        port.ip,
                        port.protocol,
                        prefix + port.process_name,
                        str(port.pid),
                        port.username,
                        port.state,
                        port.cmdline,
                    ]
                    row_key = _make_row_key(port)
                else:
                    row_data = [
                        "",
                        "",
                        "",
                        prefix + (ancestor_name or ""),
                        str(ancestor_pid or ""),
                        "",
                        "",
                        "",
                    ]
                    row_key = f"ancestor-{ancestor_pid}"
                visible_data = [
                    v for v, vis in zip(row_data, self.visible_columns) if vis
                ]
                table.add_row(*visible_data, key=row_key)
        else:
            for port in filtered:
                row_data = [
                    str(port.port),
                    port.ip,
                    port.protocol,
                    port.process_name,
                    str(port.pid),
                    port.username,
                    port.state,
                    port.cmdline,
                ]
                visible_data = [
                    v for v, vis in zip(row_data, self.visible_columns) if vis
                ]
                table.add_row(*visible_data, key=_make_row_key(port))

        # Restore cursor
        if saved_key is not None and table.row_count > 0:
            try:
                new_row_index = table.get_row_index(saved_key)
                table.cursor_coordinate = (new_row_index, 0)
            except (KeyError, IndexError, RowDoesNotExist):
                pass


def main() -> None:
    """Entry point."""
    PortUIApp().run()
