#!/usr/bin/env python3
"""Standalone curses-based live system monitor with Dracula Pro aesthetic."""

import curses
import subprocess
import time

try:
    import psutil
    HAS_PSUTIL = True
except ImportError:
    HAS_PSUTIL = False

# Dracula Pro 256-color palette
C_BG, C_FG, C_PURPLE, C_CYAN = 236, 255, 141, 43
C_GREEN, C_ORANGE, C_RED, C_YELLOW, C_GREY = 84, 209, 196, 220, 240

BLOCK_CHARS = "░▒▓█"
_prev_cpu = None


def init_colors():
    curses.start_color()
    curses.use_default_colors()
    pairs = [
        (1, C_FG, C_BG), (2, C_PURPLE, C_BG), (3, C_CYAN, C_BG),
        (4, C_GREEN, C_BG), (5, C_ORANGE, C_BG), (6, C_RED, C_BG),
        (7, C_YELLOW, C_BG), (8, C_GREY, C_BG), (9, C_BG, C_PURPLE),
    ]
    for idx, fg, bg in pairs:
        curses.init_pair(idx, fg, bg)


def make_bar(pct, width, color_pair):
    """Build a gradient bar string of given width for a percentage 0-100."""
    filled = pct / 100.0 * width
    bar = ""
    for i in range(width):
        if i < int(filled):
            bar += BLOCK_CHARS[3]
        elif i < int(filled) + 1 and filled % 1 > 0:
            idx = min(int((filled % 1) * 3), 2)
            bar += BLOCK_CHARS[idx]
        else:
            bar += " "
    return bar, color_pair


def safe_addstr(scr, y, x, text, attr=0):
    h, w = scr.getmaxyx()
    if y < 0 or y >= h or x >= w:
        return
    text = text[:max(0, w - x)]
    if text:
        try:
            scr.addstr(y, x, text, attr)
        except curses.error:
            pass


def get_cpu():
    """Return (total_pct, [per_core_pct])."""
    global _prev_cpu
    if HAS_PSUTIL:
        per = psutil.cpu_percent(interval=0, percpu=True)
        return sum(per) / len(per) if per else 0, per

    # Fallback: /proc/stat
    try:
        with open("/proc/stat") as f:
            lines = [l for l in f if l.startswith("cpu")]
        current = {}
        for line in lines:
            parts = line.split()
            name = parts[0]
            vals = list(map(int, parts[1:]))
            idle = vals[3] + (vals[4] if len(vals) > 4 else 0)
            total = sum(vals)
            current[name] = (idle, total)

        if _prev_cpu is None:
            _prev_cpu = current
            return 0, [0]

        cores = []
        for name in sorted(current):
            if name == "cpu":
                continue
            if name in _prev_cpu:
                di = current[name][0] - _prev_cpu[name][0]
                dt = current[name][1] - _prev_cpu[name][1]
                cores.append(max(0, (1 - di / dt) * 100) if dt else 0)

        prev_cpu_total = _prev_cpu.get("cpu", (0, 0))
        cur_cpu_total = current.get("cpu", (0, 0))
        dt = cur_cpu_total[1] - prev_cpu_total[1]
        di = cur_cpu_total[0] - prev_cpu_total[0]
        total = max(0, (1 - di / dt) * 100) if dt else 0

        _prev_cpu = current
        return total, cores
    except Exception:
        return 0, [0]


def get_ram():
    """Return (used_gb, total_gb, pct)."""
    if HAS_PSUTIL:
        m = psutil.virtual_memory()
        return m.used / 2**30, m.total / 2**30, m.percent
    try:
        info = {}
        with open("/proc/meminfo") as f:
            for line in f:
                k, v = line.split(":")
                info[k.strip()] = int(v.split()[0])
        total = info["MemTotal"]
        avail = info.get("MemAvailable", info["MemFree"])
        used = total - avail
        pct = used / total * 100 if total else 0
        return used / 2**20, total / 2**20, pct
    except Exception:
        return 0, 0, 0


def get_gpu():
    """Return dict with gpu_util, vram_used, vram_total, temp or None."""
    try:
        out = subprocess.check_output(
            ["nvidia-smi",
             "--query-gpu=utilization.gpu,memory.used,memory.total,temperature.gpu",
             "--format=csv,noheader,nounits"],
            stderr=subprocess.DEVNULL, timeout=2
        ).decode().strip()
        vals = [v.strip() for v in out.split(",")]
        return {
            "util": float(vals[0]), "vram_used": float(vals[1]),
            "vram_total": float(vals[2]), "temp": float(vals[3]),
        }
    except Exception:
        return None


def draw_bar_line(scr, y, x, label, pct, width, color, suffix=""):
    """Draw: LABEL [████░░░░░░] PCT% SUFFIX"""
    safe_addstr(scr, y, x, label, curses.color_pair(8))
    bar_text, cp = make_bar(pct, width, color)
    safe_addstr(scr, y, x + len(label), bar_text, curses.color_pair(cp))
    info = f" {pct:5.1f}% {suffix}"
    safe_addstr(scr, y, x + len(label) + width, info, curses.color_pair(1))


def draw(scr):
    curses.curs_set(0)
    scr.nodelay(True)
    scr.timeout(1000)
    init_colors()

    # Initial CPU read for /proc/stat delta
    if not HAS_PSUTIL:
        get_cpu()
        time.sleep(0.1)

    while True:
        scr.bkgd(" ", curses.color_pair(1))
        scr.erase()
        h, w = scr.getmaxyx()
        bar_w = min(40, w - 30)
        if bar_w < 5:
            bar_w = 5

        y = 0
        # Header
        ts = time.strftime("%H:%M:%S")
        header = f"  System Monitor"
        safe_addstr(scr, y, 0, header, curses.color_pair(9) | curses.A_BOLD)
        safe_addstr(scr, y, len(header), " " * max(0, w - len(header) - len(ts) - 1), curses.color_pair(9))
        safe_addstr(scr, y, max(len(header), w - len(ts) - 1), ts, curses.color_pair(9))
        y += 2

        # CPU
        cpu_total, cpu_cores = get_cpu()
        safe_addstr(scr, y, 1, "CPU", curses.color_pair(2) | curses.A_BOLD)
        y += 1
        draw_bar_line(scr, y, 2, "Total ", cpu_total, bar_w, 3)
        y += 1

        # Per-core: 2 columns if many cores
        cols = 2 if len(cpu_cores) > 4 else 1
        col_w = (w - 2) // cols
        core_bar_w = min(20 if cols == 2 else bar_w, col_w - 18)
        if core_bar_w < 3:
            core_bar_w = 3
        for i, pct in enumerate(cpu_cores):
            col = i % cols
            row = y + i // cols
            cx = 2 + col * col_w
            label = f"C{i:<2d} "
            draw_bar_line(scr, row, cx, label, pct, core_bar_w, 3)
        y += (len(cpu_cores) + cols - 1) // cols + 1

        # RAM
        ram_used, ram_total, ram_pct = get_ram()
        safe_addstr(scr, y, 1, "RAM", curses.color_pair(2) | curses.A_BOLD)
        y += 1
        suffix = f"{ram_used:.1f}/{ram_total:.1f} GB"
        draw_bar_line(scr, y, 2, "Mem  ", ram_pct, bar_w, 4, suffix)
        y += 2

        # GPU
        gpu = get_gpu()
        safe_addstr(scr, y, 1, "GPU", curses.color_pair(2) | curses.A_BOLD)
        y += 1
        if gpu:
            draw_bar_line(scr, y, 2, "Util ", gpu["util"], bar_w, 5)
            y += 1
            vram_pct = gpu["vram_used"] / gpu["vram_total"] * 100 if gpu["vram_total"] else 0
            suffix = f"{gpu['vram_used']:.0f}/{gpu['vram_total']:.0f} MB"
            draw_bar_line(scr, y, 2, "VRAM ", vram_pct, bar_w, 7, suffix)
            y += 1
            temp = gpu["temp"]
            tc = 4 if temp < 60 else (7 if temp < 80 else 6)
            safe_addstr(scr, y, 2, f"Temp  {temp:.0f}°C", curses.color_pair(tc))
        else:
            safe_addstr(scr, y, 2, "No GPU detected", curses.color_pair(8))
        y += 2

        # Footer
        footer = " q:quit  r:refresh "
        safe_addstr(scr, h - 1, 0, footer, curses.color_pair(8))

        scr.refresh()

        key = scr.getch()
        if key == ord("q"):
            break
        elif key == ord("r"):
            continue


if __name__ == "__main__":
    curses.wrapper(draw)
