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

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_device_breakdown,
    calculate_process_gpu_metrics,
    format_device_breakdown,
    recommended_device_display_limit,
    summarize_process_device_resources,
)


class ProcessPanel:
    """Render AI process information in a scrollable table."""

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

        Args:
            display: Display instance
        """
        self.display = display
        self.base_headers = {
            "PID": 7,
            "CPU%": 7,
            "MEM%": 7,
            "ACC%": 7,
            "AMEM%": 7,
            "Device": 18,
            "R/sMB": 7,
            "W/sMB": 7,
            "NET": 5,
            "STATUS": 8,
            "Process": 20,
        }
        self.headers = self.base_headers.copy()
        self._last_reserved_rows = 2

    def _theme_attr(self, role: str, fallback_pair: int = 0) -> int:
        """Resolve theme attribute with compatibility fallback for test doubles."""
        getter = getattr(self.display, "get_theme_attr", None)
        if callable(getter):
            return int(getter(role))
        return curses.color_pair(fallback_pair)

    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 tree row list of ``(process, depth)`` pairs."""
        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)`` pairs."""
        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 _build_headers(
        self, width: int, total_accelerators: int
    ) -> Tuple[Dict[str, int], int]:
        """Create dynamic headers sized for current width and hardware count."""
        headers = self.base_headers.copy()
        device_limit = recommended_device_display_limit(total_accelerators)
        device_width = max(12, min(22, 8 + (5 * max(1, device_limit))))
        if total_accelerators > device_limit:
            device_width = min(22, device_width + 3)
        headers["Device"] = device_width

        fixed_width = sum(
            value for key, value in headers.items() if key != "Process"
        ) + (len(headers) - 1)
        headers["Process"] = max(16, width - fixed_width - 1)
        return headers, device_limit

    def _render_device_resource_overview(
        self,
        processes: List[Dict[str, Any]],
        accelerators: List[Any],
        y: int,
        indent: int,
        device_limit: int,
    ) -> int:
        """Render a compact grouped device usage summary for AI processes."""
        summaries = summarize_process_device_resources(processes, accelerators)
        if not summaries:
            return y

        visible_summaries = summaries[:device_limit]
        hidden_count = max(0, len(summaries) - len(visible_summaries))
        parts: List[str] = []
        for summary in visible_summaries:
            used_gb = float(summary.get("memory_used_mb", 0.0)) / 1024
            total_mb = float(summary.get("memory_total_mb", 0.0))
            if total_mb > 0:
                total_text = f"{total_mb / 1024:.1f}G"
                mem_text = f"{used_gb:.1f}/{total_text}"
            else:
                mem_text = f"{used_gb:.1f}G"
            parts.append(
                (
                    f"{summary['device']} {int(summary['process_count'])}p "
                    f"{float(summary['utilization']):.0f}% {mem_text}"
                )
            )

        if hidden_count > 0:
            parts.append(f"+{hidden_count}")

        line = "By Device: " + " | ".join(parts)
        max_width = max(20, self.display.width - indent - 1)
        if len(line) > max_width:
            line = line[: max(0, max_width - 3)] + "..."

        self.display.safe_addstr(
            y, indent, line, self._theme_attr("chip", fallback_pair=5)
        )
        return y + 1

    def _render_header(self, headers: Dict[str, int], y: int, indent: int = 2) -> int:
        """Render the process list header row."""
        header = ""
        for col, width in headers.items():
            if col in {"Process", "Device"}:
                header += f"{col:<{width}} "
            else:
                header += f"{col:>{width}} "

        header_attr = self._theme_attr("header", fallback_pair=1) | curses.A_BOLD
        self.display.safe_addstr(y, indent, header, header_attr)
        return y + 1

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

        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 = 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))
        cpu_percent = float(process.get("cpu_percent", 0.0))
        mem_percent = float(process.get("memory_percent", 0.0))
        accel_percent = max(0.0, min(100.0, float(gpu_util)))
        accel_mem_percent = max(0.0, min(100.0, float(vram_percent)))
        status = str(process.get("status", ""))[: headers["STATUS"]]
        device_width = headers["Device"]
        if len(device_display) > device_width:
            device_display = device_display[: max(0, device_width - 3)] + "..."

        pid_display = int(process.get("pid", 0))
        line = (
            f"{pid_display:7d} "
            f"{cpu_percent:7.1f} "
            f"{mem_percent:7.1f} "
            f"{accel_percent:7.1f} "
            f"{accel_mem_percent:7.1f} "
            f"{device_display:<{device_width}} "
            f"{io_read_mb_s:7.2f} "
            f"{io_write_mb_s:7.2f} "
            f"{net_connections:5d} "
            f"{status:>8} "
            f"{name_display:<{name_width}}"
        )

        # Apply selection highlighting
        attr = (
            self._theme_attr("selected", fallback_pair=2)
            if selected
            else self._theme_attr("info", fallback_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, self._theme_attr("muted", fallback_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, self._theme_attr("muted", fallback_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,
        accelerator_info: Optional[List[Tuple[GPUInfo, str]]] = None,
    ) -> int:
        """Render the complete process panel for the current viewport.

        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
            accelerator_info: Optional (accelerator, vendor) tuples

        Returns:
            Final Y coordinate after rendering
        """
        accelerator_entries: List[Any]
        if accelerator_info is not None:
            accelerator_entries = list(accelerator_info)
        else:
            accelerator_entries = list(gpus)

        # Build rows in current view mode (flat or tree)
        process_rows = self._get_display_rows(
            processes, sort_by, sort_reverse, tree_mode
        )

        # Build dynamic headers from width + hardware count.
        available_width = max(40, self.display.width - indent - 1)
        headers, device_limit = self._build_headers(
            available_width, len(accelerator_entries)
        )
        self.headers = headers

        y = start_y
        y = self._render_device_resource_overview(
            processes, accelerator_entries, y, indent, device_limit
        )
        y = self._render_header(headers, y, indent)
        self._last_reserved_rows = max(1, y - start_y)

        # 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)
            device_breakdown = calculate_process_device_breakdown(
                process,
                accelerator_entries,
                max_devices=max(1, len(accelerator_entries)),
            )
            device_display = format_device_breakdown(
                device_breakdown,
                max_devices=device_limit,
                include_utilization=True,
                include_memory=False,
            )
            if not device_display:
                device_display = "CPU"

            is_selected = (start_idx + idx) == selected_index
            y = self._render_process_line(
                process,
                gpu_util,
                vram_percent,
                device_display,
                headers,
                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 the maximum scroll position for the current view."""
        reserved_rows = max(2, self._last_reserved_rows + 1)
        visible_height = self.display.height - start_y - reserved_rows
        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 and return the updated scroll position."""
        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
