#!/usr/bin/env python3
"""Overview panel component combining GPU, process, and memory information."""

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

from ...application.process_topology import summarize_job_topology
from ...core.gpu.base import GPUInfo
from ...core.system.cpu import CPUStats
from ...core.system.memory import MemoryStats
from ..display import Display
from .process_metrics import (
    build_accelerator_identities,
    calculate_process_device_breakdown,
    calculate_process_gpu_metrics,
    format_device_breakdown,
    recommended_device_display_limit,
)


class OverviewPanel:
    """Renders system overview combining all information."""

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

        Args:
            display: Display instance
        """
        self.display = display
        # Base header widths for fixed columns
        self.base_headers = {
            "PID": 7,
            "CPU%": 7,
            "MEM%": 7,
            "GPU%": 7,
            "VRAM%": 7,
            "Device": 18,
            "Status": 8,
        }
        self.min_name_width = 20  # Minimum width for process name

    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)

    @staticmethod
    def _memory_marker(gpu: GPUInfo) -> str:
        """Return a compact memory-model marker for display labels."""
        model = str(getattr(gpu, "memory_model", "")).strip().lower()
        if model == "shared":
            return "~"
        if model == "hybrid":
            return "+"
        return " "

    def _sparkline(self, values: List[float], width: int, max_value: float) -> str:
        """Build a compact sparkline with adaptive scaling."""
        if width <= 0:
            return ""
        if not values:
            return " " * width

        # Resample if we have more points than horizontal space.
        if len(values) > width:
            step = len(values) / width
            sample: List[float] = []
            for index in range(width):
                start = int(index * step)
                end = int((index + 1) * step)
                if end <= start:
                    end = start + 1
                chunk = values[start:end]
                sample.append(sum(chunk) / len(chunk))
        else:
            sample = values[-width:]

        # Keep left padding empty when history is shorter than width.
        left_padding = width - len(sample)
        if left_padding > 0:
            sample = ([float("nan")] * left_padding) + sample

        numeric_sample = [value for value in sample if not math.isnan(value)]
        if not numeric_sample:
            return " " * width

        # Adaptive scale for better local shape visibility while respecting bounds.
        local_min = max(0.0, min(numeric_sample))
        local_max = min(max_value, max(numeric_sample))
        if local_max - local_min < 10.0:
            midpoint = (local_min + local_max) / 2.0
            local_min = max(0.0, midpoint - 5.0)
            local_max = min(max_value, midpoint + 5.0)

        span = max(1.0, local_max - local_min)
        levels = "▁▂▃▄▅▆▇█"
        chars = []
        for value in sample:
            if math.isnan(value):
                chars.append(" ")
                continue
            normalized = (max(local_min, min(local_max, value)) - local_min) / span
            level = int(round(normalized * (len(levels) - 1)))
            chars.append(levels[level])
        return "".join(chars)

    def _trend_arrow(self, values: List[float]) -> str:
        """Return trend indicator arrow for recent values."""
        if len(values) < 2:
            return "→"
        # Compare short moving averages for stability.
        tail = values[-3:] if len(values) >= 3 else values[-2:]
        head = values[-6:-3] if len(values) >= 6 else values[: -len(tail)]
        if not head:
            head = values[:1]
        old_avg = sum(head) / len(head)
        new_avg = sum(tail) / len(tail)
        delta = new_avg - old_avg
        if delta > 1.0:
            return "↑"
        if delta < -1.0:
            return "↓"
        return "→"

    def _render_history_overview(
        self,
        history: Dict[str, List[float]],
        y: int,
        x: int,
        width: int,
    ) -> int:
        """Render recent metric history lines."""
        self.display.safe_addstr(
            y, x, "Recent Activity (latest)", curses.color_pair(1) | curses.A_BOLD
        )
        y += 1

        spark_width = max(8, min(30, width - 18))
        cpu_values = history.get("cpu", [])
        mem_values = history.get("memory", [])
        gpu_values = history.get("gpu", [])

        cpu_spark = self._sparkline(cpu_values, spark_width, 100.0)
        mem_spark = self._sparkline(mem_values, spark_width, 100.0)
        gpu_spark = self._sparkline(gpu_values, spark_width, 100.0)

        cpu_now = cpu_values[-1] if cpu_values else 0.0
        mem_now = mem_values[-1] if mem_values else 0.0
        gpu_now = gpu_values[-1] if gpu_values else 0.0
        cpu_trend = self._trend_arrow(cpu_values)
        mem_trend = self._trend_arrow(mem_values)
        gpu_trend = self._trend_arrow(gpu_values)

        self.display.safe_addstr(
            y,
            x,
            f"CPU [{cpu_spark}] {cpu_now:5.1f}% {cpu_trend}",
            curses.color_pair(6),
        )
        y += 1
        self.display.safe_addstr(
            y,
            x,
            f"MEM [{mem_spark}] {mem_now:5.1f}% {mem_trend}",
            curses.color_pair(6),
        )
        y += 1
        self.display.safe_addstr(
            y,
            x,
            f"GPU [{gpu_spark}] {gpu_now:5.1f}% {gpu_trend}",
            curses.color_pair(6),
        )
        return y + 2

    def _render_gpu_overview(
        self,
        gpu: GPUInfo,
        vendor: str,
        y: int,
        x: int,
        width: int,
        device_display_index: Optional[int] = None,
    ) -> int:
        """Render accelerator overview section.

        Args:
            gpu: GPU information
            vendor: GPU vendor string
            y: Starting Y coordinate
            x: Starting X coordinate
            width: Available width

        Returns:
            Next Y coordinate
        """
        device_label = "NPU" if vendor.endswith("_npu") else "GPU"

        # Title
        self.display.safe_addstr(
            y, x, f"{device_label} Status", curses.color_pair(1) | curses.A_BOLD
        )
        y += 1

        # Accelerator model with index
        vendor_prefix = ""
        if all(v.upper() not in gpu.name.upper() for v in ["NVIDIA", "AMD", "INTEL"]):
            vendor_prefix = f"{vendor.upper()} "
        display_index = (
            int(gpu.index)
            if device_display_index is None
            else int(device_display_index)
        )
        self.display.safe_addstr(
            y,
            x,
            f"{device_label} {display_index}: {vendor_prefix}{gpu.name}",
            curses.color_pair(5),
        )
        y += 1

        # Framework versions (if available)
        version_parts = []
        if gpu.cuda_version:
            version_parts.append(f"CUDA: {gpu.cuda_version}")
        if gpu.rocm_version:
            version_parts.append(f"ROCm: {gpu.rocm_version}")
        if gpu.driver_version:
            version_parts.append(f"Driver: {gpu.driver_version}")

        if version_parts:
            version_line = "       " + "  ".join(version_parts)
            self.display.safe_addstr(y, x, version_line, curses.color_pair(6))
            y += 1

        y += 1

        # Accelerator metrics - calculate bar widths based on actual line format
        # Util line format: "GPU|NPU [bar] 100.0%"
        # = 7 + 2 + bar + 1 + 6 = 16 + bar
        gpu_bar_width = max(0, min(30, width - 16))

        # Utilization
        utilization_percent = max(0.0, min(100.0, float(gpu.utilization)))
        bar, color = self.display.create_bar(utilization_percent, gpu_bar_width)
        line = f"{device_label[:4]:<4}   [{bar}] {utilization_percent:5.1f}%"
        self.display.safe_addstr(y, x, line, color)
        y += 1

        # Memory
        known_total = gpu.memory_total > 0
        mem_percent = (gpu.memory_used / gpu.memory_total * 100) if known_total else 0
        mem_percent = max(0.0, min(100.0, float(mem_percent)))
        memory_label_base = "MEM" if vendor.endswith("_npu") else "VRAM"
        memory_label = f"{memory_label_base}{self._memory_marker(gpu)}"
        # Memory line format:
        # "VRAM+  [bar]  12.5/ 24.0GB ( 52.1%)" or
        # "MEM~   [bar]   1.2/  N/A  (  N/A)"
        vram_bar_width = max(0, min(30, width - 37))
        bar, color = self.display.create_bar(mem_percent, vram_bar_width)
        if memory_label.endswith(("~", "+")):
            color = self._theme_attr("warning", fallback_pair=3) | curses.A_BOLD
        elif not known_total:
            color = self._theme_attr("muted", fallback_pair=6)

        # Convert memory values from MB to GB for display
        mem_used_gb = gpu.memory_used / 1024
        mem_total_gb = gpu.memory_total / 1024
        if known_total:
            line = (
                f"{memory_label:<5} [{bar}] {mem_used_gb:5.1f}/{mem_total_gb:5.1f}GB "
                f"({mem_percent:5.1f}%)"
            )
        else:
            line = f"{memory_label:<5} [{bar}] {mem_used_gb:5.1f}/  N/A  (  N/A)"
        self.display.safe_addstr(y, x, line, color)
        y += 1

        # Temperature
        if gpu.temperature > 0:
            line = f"Temp   {gpu.temperature:5.1f}°C"
            self.display.safe_addstr(
                y, x, line, self.display.get_color(gpu.temperature)
            )
            y += 1

        return y + 1

    def _render_memory_overview(
        self, memory_stats: MemoryStats, y: int, x: int, width: int
    ) -> int:
        """Render memory overview section.

        Args:
            memory_stats: Memory statistics
            y: Starting Y coordinate
            x: Starting X coordinate
            width: Available width

        Returns:
            Next Y coordinate
        """
        # Title
        self.display.safe_addstr(
            y, x, "System Memory", curses.color_pair(1) | curses.A_BOLD
        )
        y += 1

        # RAM usage
        # RAM line format: "RAM    [bar]  18.5/ 62.7GB ( 29.5%)"
        # = 7 + 2 + bar + 2 + 24 = 35 + bar
        mem_bar_width = max(0, min(30, width - 35))
        bar, color = self.display.create_bar(memory_stats.percent, mem_bar_width)

        ram_used_gb = memory_stats.used / (1024**3)
        ram_total_gb = memory_stats.total / (1024**3)
        line = (
            f"RAM    [{bar}] {ram_used_gb:5.1f}/{ram_total_gb:5.1f}GB "
            f"({memory_stats.percent:5.1f}%)"
        )
        self.display.safe_addstr(y, x, line, color)
        y += 1

        # Swap usage if available
        if memory_stats.swap_total > 0:
            swap_used_gb = memory_stats.swap_used / (1024**3)
            swap_total_gb = memory_stats.swap_total / (1024**3)
            # Swap line format same as RAM: 35 + bar
            swap_bar_width = max(0, min(30, width - 35))
            bar, color = self.display.create_bar(
                memory_stats.swap_percent, swap_bar_width
            )
            line = (
                f"Swap   [{bar}] {swap_used_gb:5.1f}/{swap_total_gb:5.1f}GB "
                f"({memory_stats.swap_percent:5.1f}%)"
            )
            self.display.safe_addstr(y, x, line, color)
            y += 1

        return y

    def _render_process_overview(
        self,
        processes: List[Dict[str, Any]],
        gpus: List[GPUInfo],
        y: int,
        x: int,
        width: int,
        max_processes: int = 5,
        selected_index: int = 0,
        accelerator_info: Optional[List[tuple[GPUInfo, str]]] = None,
    ) -> int:
        """Render process overview section.

        Args:
            processes: List of process information
            gpus: List of GPU information
            accelerator_info: List of (accelerator, vendor) for device attribution
            y: Starting Y coordinate
            x: Starting X coordinate
            width: Available width
            max_processes: Maximum number of processes to show
            selected_index: Index of selected process for highlighting

        Returns:
            Next Y coordinate
        """
        # Title
        self.display.safe_addstr(
            y, x, "Top Processes", curses.color_pair(1) | curses.A_BOLD
        )
        y += 2

        # Calculate dynamic widths based on available space
        accelerator_entries = accelerator_info if accelerator_info is not None else gpus
        total_accelerators = len(accelerator_entries)
        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 = self.base_headers.copy()
        headers["Device"] = device_width
        total_fixed_width = sum(headers.values()) + len(headers)  # separators
        name_width = max(
            self.min_name_width, width - total_fixed_width - 2
        )  # -2 for margins

        # Create headers dict with dynamic name width
        headers["Name"] = name_width

        # Header
        header = ""
        for col, col_width in headers.items():
            # Right-align numeric columns, left-align text columns.
            if col in {"Name", "Device"}:
                header += f"{col:<{col_width}}"
            else:
                header += f"{col:>{col_width}} "
        self.display.safe_addstr(
            y, x, header, self._theme_attr("chip", fallback_pair=5)
        )
        y += 1

        # Sort locally to avoid mutating shared process state used by other views.
        sorted_processes = sorted(
            processes, key=lambda process: process["cpu_percent"], reverse=True
        )

        # Show top processes
        for idx, proc in enumerate(sorted_processes[:max_processes]):
            gpu_util, vram_percent = calculate_process_gpu_metrics(proc, gpus)
            gpu_util = max(0.0, min(100.0, float(gpu_util)))
            vram_percent = max(0.0, min(100.0, float(vram_percent)))
            device_breakdown = calculate_process_device_breakdown(
                proc,
                accelerator_entries,
                max_devices=max(1, len(accelerator_entries)),
            )

            # Format process line using dynamic width
            name_width = headers["Name"]  # Use dynamic width from headers
            # Use the summarized command line if available, otherwise use process name
            if "cmdline_summary" in proc:
                name_display = proc["cmdline_summary"]
            elif "cmdline" in proc:
                name_display = proc["cmdline"]
            else:
                name_display = proc["name"]
            if len(name_display) > name_width:
                name_display = name_display[: name_width - 3] + "..."

            device_display = format_device_breakdown(
                device_breakdown,
                max_devices=device_limit,
                include_utilization=True,
                include_memory=False,
            )
            if not device_display:
                device_display = "CPU"
            device_width = headers["Device"]
            if len(device_display) > device_width:
                device_display = device_display[: max(0, device_width - 3)] + "..."

            # Determine process status
            status_value = str(proc.get("status", "")).strip().lower()
            if not status_value:
                status_value = str(proc.get("raw_status", "")).strip().lower()
            if not status_value:
                status_value = "running" if proc["cpu_percent"] > 0.1 else "sleeping"
            if status_value.startswith("run"):
                status = "running"
            elif "sleep" in status_value:
                status = "sleeping"
            elif status_value.startswith("disk"):
                status = "diskwait"
            else:
                status = status_value[:8]
            status_color = (
                self._theme_attr("normal", fallback_pair=2)
                if status == "running"
                else self._theme_attr("muted", fallback_pair=6)
            )

            # Format base metrics
            base_metrics = (
                f"{proc['pid']:7d} "
                f"{proc['cpu_percent']:7.1f} "
                f"{proc['memory_percent']:7.1f} "
                f"{gpu_util:7.1f} "
                f"{vram_percent:7.1f} "
            )

            # Format status and name
            device_field = f"{device_display:<{device_width}}"
            status_field = f"{status:8} "  # Added space after status
            name_field = f"{name_display:<{name_width}}"

            # Determine if this process is selected
            is_selected = idx == selected_index

            # Write with different colors for status and selection highlighting
            if is_selected:
                # Entire line with selected-theme highlight
                line = base_metrics + device_field + status_field + name_field
                self.display.safe_addstr(
                    y, x, line, self._theme_attr("selected", fallback_pair=2)
                )
            else:
                # Normal rendering with different colors
                base_attr = self._theme_attr("info", fallback_pair=6)
                self.display.safe_addstr(y, x, base_metrics, base_attr)
                self.display.safe_addstr(
                    y, x + len(base_metrics), device_field, base_attr
                )
                self.display.safe_addstr(
                    y,
                    x + len(base_metrics) + len(device_field),
                    status_field,
                    status_color,
                )
                self.display.safe_addstr(
                    y,
                    x + len(base_metrics) + len(device_field) + len(status_field),
                    name_field,
                    base_attr,
                )

            y += 1

        return y

    def _render_job_overview(
        self,
        processes: List[Dict[str, Any]],
        y: int,
        x: int,
        width: int,
        max_jobs: int = 3,
    ) -> int:
        """Render a concise AI job-topology summary."""
        summaries = summarize_job_topology(processes, max_jobs=max_jobs)
        if not summaries:
            return y

        self.display.safe_addstr(y, x, "AI Jobs", curses.color_pair(1) | curses.A_BOLD)
        y += 1

        for summary in summaries:
            job_id = str(summary.get("job_id", "unknown"))
            workers = int(summary.get("size", 0))
            stragglers = int(summary.get("stragglers", 0))
            distributed = bool(summary.get("is_distributed", False))
            mode = "dist" if distributed else "single"
            line = f"{job_id} | workers:{workers} | stragglers:{stragglers} | {mode}"
            if len(line) > width:
                line = line[: max(0, width - 3)] + "..."

            color = curses.color_pair(3) if stragglers > 0 else curses.color_pair(6)
            self.display.safe_addstr(y, x, line, color)
            y += 1

        return y + 1

    def _render_dependency_warnings(
        self,
        dependency_warnings: List[str],
        y: int,
        x: int,
        width: int,
        max_items: int = 4,
    ) -> int:
        """Render missing dependency notices in overview when present."""
        if not dependency_warnings:
            return y

        self.display.safe_addstr(
            y, x, "Dependencies", curses.color_pair(1) | curses.A_BOLD
        )
        y += 1

        for warning in dependency_warnings[:max_items]:
            line = f"- {warning}"
            if len(line) > width:
                line = line[: max(0, width - 3)] + "..."
            self.display.safe_addstr(y, x, line, curses.color_pair(3))
            y += 1

        if len(dependency_warnings) > max_items:
            extra = len(dependency_warnings) - max_items
            line = f"... and {extra} more"
            self.display.safe_addstr(y, x, line, curses.color_pair(6))
            y += 1

        return y + 1

    def _render_cpu_overview(
        self, cpu_stats: CPUStats, y: int, x: int, width: int
    ) -> int:
        """Render CPU overview section.

        Args:
            cpu_stats: CPU statistics
            y: Starting Y coordinate
            x: Starting X coordinate
            width: Available width

        Returns:
            Next Y coordinate
        """
        # Title
        self.display.safe_addstr(
            y, x, "CPU Status", curses.color_pair(1) | curses.A_BOLD
        )
        y += 1

        # CPU model
        self.display.safe_addstr(
            y, x, f"Model: {cpu_stats.model}", curses.color_pair(5)
        )
        y += 2

        # CPU utilization
        # CPU line format: "CPU    [bar]  28.0%"
        # = 7 + 2 + bar + 2 + 5 = 16 + bar
        cpu_bar_width = max(0, min(30, width - 16))
        bar, color = self.display.create_bar(cpu_stats.total_percent, cpu_bar_width)
        line = f"CPU    [{bar}] {cpu_stats.total_percent:5.1f}%"
        self.display.safe_addstr(y, x, line, color)
        y += 1

        # CPU temperature if available
        if cpu_stats.temperature > 0:
            line = f"Temp   {cpu_stats.temperature:5.1f}°C"
            self.display.safe_addstr(
                y, x, line, self.display.get_color(cpu_stats.temperature)
            )
            y += 1

        if cpu_stats.current_freq > 0:
            line = (
                f"Freq   {cpu_stats.current_freq/1000:4.2f}GHz "
                f"({cpu_stats.min_freq/1000:4.2f}-{cpu_stats.max_freq/1000:4.2f})"
            )
            self.display.safe_addstr(y, x, line, curses.color_pair(6))
            y += 1

        # Load averages
        line = (
            f"Load   {cpu_stats.load_1min:5.2f} "
            f"{cpu_stats.load_5min:5.2f} {cpu_stats.load_15min:5.2f}"
        )
        self.display.safe_addstr(y, x, line, curses.color_pair(6))
        y += 1

        return y + 1

    def render(
        self,
        gpu_info: List[tuple[GPUInfo, str]],
        processes: List[Dict[str, Any]],
        memory_stats: MemoryStats,
        cpu_stats: CPUStats,
        primary_vendor: str,
        start_y: int = 3,
        selected_index: int = 0,
        history: Optional[Dict[str, List[float]]] = None,
        dependency_warnings: Optional[List[str]] = None,
    ) -> None:
        """Render the complete overview panel.

        Args:
            gpu_info: List of tuples containing (GPU information, vendor string)
            processes: List of process information
            memory_stats: Memory statistics
            cpu_stats: CPU statistics
            primary_vendor: Primary GPU vendor string
            start_y: Starting Y coordinate (ignored, uses layout system)
            selected_index: Index of selected process for highlighting
        """
        # Get layout from centralized layout calculator
        layout = self.display.layout.calculate()
        left_rect = layout["overview_left"]
        right_rect = layout["overview_right"]
        separator_x = layout["overview_separator_x"]

        # Check if we're using split layout (separator_x >= 0)
        accelerator_identities = build_accelerator_identities(gpu_info)
        if separator_x >= 0:
            # Draw vertical separator from content start to footer
            content_rect = layout["content"]
            for row in range(content_rect.y, content_rect.bottom):
                self.display.safe_addstr(row, separator_x, "│", curses.color_pair(1))

            # Left panel (System Information)
            # Apply left margin to content
            left_inner = left_rect.inner(left=self.display.layout.CONTENT_MARGIN_LEFT)
            y = left_inner.y
            y = self._render_cpu_overview(cpu_stats, y, left_inner.x, left_inner.width)
            y += 1
            if history:
                y = self._render_history_overview(
                    history, y, left_inner.x, left_inner.width
                )
            if dependency_warnings:
                y = self._render_dependency_warnings(
                    dependency_warnings, y, left_inner.x, left_inner.width
                )

            # Render all detected GPUs
            for (gpu, vendor), identity in zip(gpu_info, accelerator_identities):
                y = self._render_gpu_overview(
                    gpu,
                    vendor,
                    y,
                    left_inner.x,
                    left_inner.width,
                    device_display_index=int(identity.get("global_index", gpu.index)),
                )
                y += 1

            y = self._render_memory_overview(
                memory_stats, y, left_inner.x, left_inner.width
            )

            # Right panel (Process Information)
            # Right panel already includes proper spacing from separator
            process_y = self._render_job_overview(
                processes,
                right_rect.y,
                right_rect.x,
                right_rect.width,
            )
            self._render_process_overview(
                processes,
                [gpu for gpu, _ in gpu_info],
                process_y,
                right_rect.x,
                right_rect.width,
                selected_index=selected_index,
                accelerator_info=gpu_info,
            )
            return

        # Stacked layout for narrow windows (use full content width)
        content = self.display.layout.get_content_area()
        y = content.y

        # Render system information
        y = self._render_cpu_overview(cpu_stats, y, content.x, content.width)
        y += 1
        if history:
            y = self._render_history_overview(history, y, content.x, content.width)
        if dependency_warnings:
            y = self._render_dependency_warnings(
                dependency_warnings, y, content.x, content.width
            )

        # Render all detected GPUs
        for (gpu, vendor), identity in zip(gpu_info, accelerator_identities):
            y = self._render_gpu_overview(
                gpu,
                vendor,
                y,
                content.x,
                content.width,
                device_display_index=int(identity.get("global_index", gpu.index)),
            )
            y += 1

        y = self._render_memory_overview(memory_stats, y, content.x, content.width)
        y += 1

        # Render process information
        y = self._render_job_overview(processes, y, content.x, content.width)
        self._render_process_overview(
            processes,
            [gpu for gpu, _ in gpu_info],
            y,
            content.x,
            content.width,
            selected_index=selected_index,
            accelerator_info=gpu_info,
        )

    def get_selected_process(
        self, processes: List[Dict[str, Any]], selected_index: int
    ) -> Optional[Dict[str, Any]]:
        """Get the selected process from the overview top processes list.

        Args:
            processes: List of all process information
            selected_index: Index of selected process (0-4 for top 5)

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

        # Sort processes by CPU usage (same as render)
        sorted_processes = sorted(
            processes, key=lambda p: p["cpu_percent"], reverse=True
        )

        # Get top 5 processes
        top_processes = sorted_processes[:5]

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

        return None
