#!/usr/bin/env python3
"""Process information panel component."""

import curses
from typing import Any, Dict, List, Optional, Tuple

from ...core.gpu.base import GPUInfo
from ..display import Display
from .process_metrics import calculate_process_gpu_metrics


class ProcessPanel:
    """Renders AI process information panel."""

    def __init__(self, display: Display):
        """Initialize the process panel.

        Args:
            display: Display instance
        """
        self.display = display
        self.headers = {
            "PID": 7,
            "CPU%": 7,
            "MEM%": 7,
            "GPU%": 7,
            "VRAM%": 7,
            "R/sMB": 7,
            "W/sMB": 7,
            "NET": 5,
            "STATUS": 8,
            "Process": 20,
        }

    def _build_process_tree_rows(
        self, processes: List[Dict[str, Any]], sort_by: str, sort_reverse: bool
    ) -> List[Tuple[Dict[str, Any], int]]:
        """Build a deterministic process tree row list (process, depth)."""
        if not processes:
            return []

        pid_to_process = {proc["pid"]: proc for proc in processes}
        children_map: Dict[int, List[Dict[str, Any]]] = {}
        roots: List[Dict[str, Any]] = []

        for process in processes:
            parent_pid_raw = process.get("ppid")
            parent_pid = (
                int(parent_pid_raw) if isinstance(parent_pid_raw, int) else None
            )
            if parent_pid is not None and parent_pid in pid_to_process:
                children_map.setdefault(parent_pid, []).append(process)
            else:
                roots.append(process)

        def sort_key(item: Dict[str, Any]) -> Any:
            return item.get(sort_by, 0)

        roots.sort(key=sort_key, reverse=sort_reverse)
        for children in children_map.values():
            children.sort(key=sort_key, reverse=sort_reverse)

        rows: List[Tuple[Dict[str, Any], int]] = []
        visited: set[int] = set()

        def walk(node: Dict[str, Any], depth: int) -> None:
            pid = int(node["pid"])
            if pid in visited:
                return
            visited.add(pid)
            rows.append((node, depth))
            for child in children_map.get(pid, []):
                walk(child, depth + 1)

        for root in roots:
            walk(root, 0)

        # Include detached/cyclic nodes if any, to avoid dropping processes.
        for process in processes:
            pid = int(process["pid"])
            if pid not in visited:
                rows.append((process, 0))

        return rows

    def _get_display_rows(
        self,
        processes: List[Dict[str, Any]],
        sort_by: str,
        sort_reverse: bool,
        tree_mode: bool,
    ) -> List[Tuple[Dict[str, Any], int]]:
        """Build render rows as (process, depth)."""
        if tree_mode:
            return self._build_process_tree_rows(processes, sort_by, sort_reverse)

        sorted_processes = sorted(
            processes, key=lambda item: item.get(sort_by, 0), reverse=sort_reverse
        )
        return [(process, 0) for process in sorted_processes]

    def _calculate_gpu_metrics(
        self, process: Dict[str, Any], gpus: List[GPUInfo]
    ) -> Tuple[float, float]:
        """Calculate GPU and VRAM percentages for a process."""
        return calculate_process_gpu_metrics(process, gpus)

    def _render_header(self, y: int, indent: int = 2) -> int:
        """Render the process list header."""
        header = ""
        for col, width in self.headers.items():
            header += f"{col:>{width if col != 'Process' else ''}} "

        self.display.safe_addstr(
            y, indent, header, curses.color_pair(1) | curses.A_BOLD
        )
        return y + 1

    def _render_process_line(
        self,
        process: Dict[str, Any],
        gpu_util: float,
        vram_percent: float,
        y: int,
        indent: int = 2,
        selected: bool = False,
        tree_depth: int = 0,
    ) -> int:
        """Render a single process line.

        Args:
            process: Process information
            gpu_util: GPU utilization percentage
            vram_percent: VRAM usage percentage
            y: Y coordinate
            indent: Indentation level
            selected: Whether this process is selected

        Returns:
            Next Y coordinate
        """
        name_width = self.headers["Process"]
        name_display = str(process.get("name", ""))
        prefix = ("  " * tree_depth) + ("|- " if tree_depth > 0 else "")
        if prefix:
            max_name_length = max(0, name_width - len(prefix))
            if len(name_display) > max_name_length:
                if max_name_length <= 3:
                    name_display = "..."
                else:
                    name_display = name_display[: max_name_length - 3] + "..."
            name_display = prefix + name_display

        if len(name_display) > name_width:
            name_display = name_display[: name_width - 3] + "..."

        io_read_mb_s = float(process.get("io_read_mb_s", 0.0))
        io_write_mb_s = float(process.get("io_write_mb_s", 0.0))
        net_connections = int(process.get("net_connections", 0))

        line = (
            f"{process['pid']:7d} "
            f"{process['cpu_percent']:7.1f} "
            f"{process['memory_percent']:7.1f} "
            f"{gpu_util:7.1f} "
            f"{vram_percent:7.1f} "
            f"{io_read_mb_s:7.2f} "
            f"{io_write_mb_s:7.2f} "
            f"{net_connections:5d} "
            f"{process['status']:>8} "
            f"{name_display:<{name_width}}"
        )

        # Apply selection highlighting
        attr = curses.A_REVERSE if selected else curses.color_pair(6)
        self.display.safe_addstr(y, indent, line, attr)

        # Render summarized command line if available
        if y + 1 < self.display.height - 1:
            if "cmdline_summary" in process:
                # Use the summarized version
                cmdline = f"  └─ {process['cmdline_summary']}"
                self.display.safe_addstr(y + 1, indent, cmdline, curses.color_pair(5))
                return y + 2
            elif "cmdline" in process:
                # Fallback to original cmdline
                cmdline = f"  └─ {process['cmdline']}"
                self.display.safe_addstr(y + 1, indent, cmdline, curses.color_pair(5))
                return y + 2

        return y + 1

    def render(
        self,
        processes: List[Dict[str, Any]],
        gpus: List[GPUInfo],
        start_y: int = 3,
        indent: int = 2,
        sort_by: str = "cpu_percent",
        sort_reverse: bool = True,
        scroll_position: int = 0,
        selected_index: int = 0,
        tree_mode: bool = False,
    ) -> int:
        """Render the complete process panel.

        Args:
            processes: List of process information dictionaries
            gpus: List of GPUInfo objects
            start_y: Starting Y coordinate
            indent: Indentation level
            sort_by: Key to sort processes by
            sort_reverse: Sort in reverse order
            scroll_position: Current scroll position
            selected_index: Index of selected process

        Returns:
            Final Y coordinate after rendering
        """
        # Build rows in current view mode (flat or tree)
        process_rows = self._get_display_rows(
            processes, sort_by, sort_reverse, tree_mode
        )

        # Render header
        y = self._render_header(start_y, indent)

        # Calculate visible area
        max_processes = self.display.height - y - 1
        start_idx = min(scroll_position, len(process_rows) - max_processes)
        if start_idx < 0:
            start_idx = 0

        visible_rows = process_rows[start_idx : start_idx + max_processes]

        # Render processes
        for idx, (process, depth) in enumerate(visible_rows):
            if y >= self.display.height - 1:
                break

            gpu_util, vram_percent = self._calculate_gpu_metrics(process, gpus)
            is_selected = (start_idx + idx) == selected_index
            y = self._render_process_line(
                process,
                gpu_util,
                vram_percent,
                y,
                indent,
                is_selected,
                tree_depth=depth,
            )

        return y

    def get_max_scroll_position(
        self, processes: List[Dict[str, Any]], start_y: int = 3
    ) -> int:
        """Calculate maximum scroll position."""
        visible_height = self.display.height - start_y - 2
        return int(max(0, len(processes) - visible_height))

    def handle_scroll(
        self, key: int, current_scroll: int, processes: List[Dict[str, Any]]
    ) -> int:
        """Handle scroll input."""
        max_scroll = self.get_max_scroll_position(processes)

        if key == curses.KEY_UP:
            return max(0, current_scroll - 1)
        elif key == curses.KEY_DOWN:
            return min(max_scroll, current_scroll + 1)

        return current_scroll

    def get_selected_process(
        self,
        processes: List[Dict[str, Any]],
        selected_index: int,
        sort_by: str = "cpu_percent",
        sort_reverse: bool = True,
        tree_mode: bool = False,
    ) -> Optional[Dict[str, Any]]:
        """Get the currently selected process.

        Args:
            processes: List of process information dictionaries
            selected_index: Index of selected process
            sort_by: Key to sort processes by
            sort_reverse: Sort in reverse order

        Returns:
            Selected process dict or None if invalid index
        """
        if not processes:
            return None

        display_rows = self._get_display_rows(
            processes, sort_by, sort_reverse, tree_mode
        )

        if 0 <= selected_index < len(display_rows):
            return display_rows[selected_index][0]

        return None
