#!/usr/bin/env python3
"""Curses-based Docker container manager with Dracula Pro colors."""

import curses
import subprocess
import time
import threading

# Dracula Pro color pairs (initialized in setup_colors)
C_HEADER = 1
C_SELECTED = 2
C_RUNNING = 3
C_STOPPED = 4
C_PAUSED = 5
C_MUTED = 6
C_CYAN = 7
C_ORANGE = 8
C_STATUSBAR = 9


def setup_colors():
    curses.start_color()
    curses.use_default_colors()
    bg, fg = 236, 255
    for i in (bg, fg, 141, 43, 84, 209, 196, 220, 240):
        curses.init_color(i, *{
            236: (300, 300, 350), 255: (1000, 1000, 1000), 141: (740, 580, 1000),
            43: (490, 980, 980), 84: (490, 980, 490), 209: (1000, 640, 380),
            196: (1000, 340, 340), 220: (1000, 920, 380), 240: (450, 450, 500),
        }[i])
    curses.init_pair(C_HEADER, 141, bg)
    curses.init_pair(C_SELECTED, bg, 141)
    curses.init_pair(C_RUNNING, 84, bg)
    curses.init_pair(C_STOPPED, 196, bg)
    curses.init_pair(C_PAUSED, 220, bg)
    curses.init_pair(C_MUTED, 240, bg)
    curses.init_pair(C_CYAN, 43, bg)
    curses.init_pair(C_ORANGE, 209, bg)
    curses.init_pair(C_STATUSBAR, 255, 240)


def docker_available():
    try:
        subprocess.run(["docker", "info"], capture_output=True, timeout=5)
        return True
    except (FileNotFoundError, subprocess.TimeoutExpired):
        return False


def docker_cmd(args, timeout=10):
    try:
        r = subprocess.run(["docker"] + args, capture_output=True, text=True, timeout=timeout)
        return r.stdout.strip(), r.returncode == 0
    except (subprocess.TimeoutExpired, FileNotFoundError):
        return "", False


def fetch_containers():
    fmt = "{{.ID}}\t{{.Names}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}"
    out, ok = docker_cmd(["ps", "-a", "--format", fmt])
    if not ok or not out:
        return []
    containers = []
    for line in out.splitlines():
        parts = line.split("\t")
        if len(parts) >= 5:
            containers.append({
                "id": parts[0], "name": parts[1], "image": parts[2],
                "status": parts[3], "ports": parts[4],
                "cpu": "-", "mem": "-",
            })
    # Fetch stats for running containers
    out, ok = docker_cmd(["stats", "--no-stream", "--format", "{{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}"], timeout=15)
    if ok and out:
        stats = {}
        for line in out.splitlines():
            p = line.split("\t")
            if len(p) >= 3:
                stats[p[0]] = (p[1], p[2])
        for c in containers:
            if c["name"] in stats:
                c["cpu"], c["mem"] = stats[c["name"]]
    return containers


def container_state(c):
    s = c["status"].lower()
    if "up" in s and "paused" in s:
        return "paused"
    if "up" in s:
        return "running"
    return "stopped"


def status_icon(state):
    return {"running": "\u25b6", "stopped": "\u25a0", "paused": "\u23f8"}.get(state, "?")


def status_color(state):
    return {
        "running": C_RUNNING, "stopped": C_STOPPED, "paused": C_PAUSED
    }.get(state, C_MUTED)


class DockerManager:
    def __init__(self, stdscr):
        self.scr = stdscr
        self.containers = []
        self.cursor = 0
        self.filter_mode = "all"  # all, running, stopped
        self.search = ""
        self.message = ""
        self.msg_time = 0
        self.running = True
        self.lock = threading.Lock()
        self.view = "main"  # main or logs
        self.log_lines = []
        self.log_scroll = 0
        self.log_name = ""

    def filtered(self):
        out = self.containers
        if self.filter_mode == "running":
            out = [c for c in out if container_state(c) == "running"]
        elif self.filter_mode == "stopped":
            out = [c for c in out if container_state(c) != "running"]
        if self.search:
            out = [c for c in out if self.search.lower() in c["name"].lower()]
        return out

    def refresh_data(self):
        while self.running:
            data = fetch_containers()
            with self.lock:
                self.containers = data
            time.sleep(3)

    def flash(self, msg):
        self.message = msg
        self.msg_time = time.time()

    def do_action(self, action, cid, name):
        _, ok = docker_cmd([action, cid])
        self.flash(f"{'OK' if ok else 'FAIL'}: {action} {name}")

    def confirm(self, prompt):
        h, w = self.scr.getmaxyx()
        self.scr.addnstr(h - 1, 0, f" {prompt} (y/n) ".ljust(w), w, curses.color_pair(C_ORANGE))
        self.scr.refresh()
        return self.scr.getch() in (ord("y"), ord("Y"))

    def prompt_search(self):
        h, w = self.scr.getmaxyx()
        self.scr.addnstr(h - 1, 0, " /".ljust(w), w, curses.color_pair(C_STATUSBAR))
        self.scr.move(h - 1, 2)
        curses.echo()
        curses.curs_set(1)
        try:
            s = self.scr.getstr(h - 1, 2, w - 3).decode("utf-8", errors="ignore")
        except Exception:
            s = ""
        curses.noecho()
        curses.curs_set(0)
        self.search = s

    def draw_header(self):
        h, w = self.scr.getmaxyx()
        with self.lock:
            n = len(self.containers)
        title = f" \U0001f433 Docker Manager  [{n} containers]  filter: {self.filter_mode}"
        if self.search:
            title += f"  search: \"{self.search}\""
        self.scr.addnstr(0, 0, title.ljust(w), w, curses.color_pair(C_HEADER) | curses.A_BOLD)

    def draw_table(self):
        h, w = self.scr.getmaxyx()
        cols = [3, 24, 28, 18, 22, 20]  # icon, name, image, status, ports, cpu/mem
        headers = [" ", "NAME", "IMAGE", "STATUS", "PORTS", "CPU / MEM"]
        y = 1
        x = 1
        for i, hdr in enumerate(headers):
            self.scr.addnstr(y, x, hdr.ljust(cols[i]), min(cols[i], w - x),
                             curses.color_pair(C_CYAN) | curses.A_BOLD)
            x += cols[i]
        self.scr.addnstr(2, 0, "\u2500" * w, w, curses.color_pair(C_MUTED))

        with self.lock:
            items = self.filtered()
        max_rows = h - 4
        if self.cursor >= len(items):
            self.cursor = max(0, len(items) - 1)

        # Viewport scrolling
        start = 0
        if self.cursor >= max_rows:
            start = self.cursor - max_rows + 1

        for idx in range(start, min(start + max_rows, len(items))):
            c = items[idx]
            y = 3 + idx - start
            if y >= h - 1:
                break
            state = container_state(c)
            selected = idx == self.cursor
            attr = curses.color_pair(C_SELECTED) | curses.A_BOLD if selected else 0

            x = 1
            # Status icon
            icon = status_icon(state)
            if not selected:
                self.scr.addnstr(y, x, icon, min(cols[0], w - x), curses.color_pair(status_color(state)))
            else:
                self.scr.addnstr(y, x, icon, min(cols[0], w - x), attr)
            x += cols[0]

            fields = [c["name"], c["image"], c["status"], c["ports"], f"{c['cpu']} / {c['mem']}"]
            for i, val in enumerate(fields):
                cw = cols[i + 1]
                text = val[:cw - 1].ljust(cw)
                if x < w:
                    self.scr.addnstr(y, x, text, min(cw, w - x), attr if selected else curses.color_pair(0))
                x += cw

    def draw_statusbar(self):
        h, w = self.scr.getmaxyx()
        if time.time() - self.msg_time < 3 and self.message:
            bar = f" {self.message}"
            self.scr.addnstr(h - 1, 0, bar.ljust(w), w, curses.color_pair(C_ORANGE))
        else:
            keys = "j/k:nav  Enter/s:start  S:stop  r:restart  p:pause  l:logs  d:rm  f:filter  /:search  q:quit"
            self.scr.addnstr(h - 1, 0, f" {keys}".ljust(w), w, curses.color_pair(C_STATUSBAR))

    def draw_logs(self):
        h, w = self.scr.getmaxyx()
        title = f" Logs: {self.log_name}  (Esc/q to close, j/k to scroll)"
        self.scr.addnstr(0, 0, title.ljust(w), w, curses.color_pair(C_HEADER) | curses.A_BOLD)
        max_lines = h - 2
        start = self.log_scroll
        for i, line in enumerate(self.log_lines[start:start + max_lines]):
            if i + 1 >= h - 1:
                break
            self.scr.addnstr(i + 1, 0, line[:w], w, curses.color_pair(0))
        info = f" Line {start + 1}-{min(start + max_lines, len(self.log_lines))}/{len(self.log_lines)}"
        self.scr.addnstr(h - 1, 0, info.ljust(w), w, curses.color_pair(C_STATUSBAR))

    def get_selected(self):
        with self.lock:
            items = self.filtered()
        if 0 <= self.cursor < len(items):
            return items[self.cursor]
        return None

    def open_logs(self):
        c = self.get_selected()
        if not c:
            return
        out, ok = docker_cmd(["logs", "--tail", "50", c["id"]], timeout=10)
        if not ok:
            self.flash("Failed to fetch logs")
            return
        self.log_lines = out.splitlines() if out else ["(no logs)"]
        self.log_scroll = max(0, len(self.log_lines) - (self.scr.getmaxyx()[0] - 2))
        self.log_name = c["name"]
        self.view = "logs"

    def handle_logs_input(self, key):
        h = self.scr.getmaxyx()[0] - 2
        if key in (27, ord("q")):  # Esc or q
            self.view = "main"
        elif key in (ord("j"), curses.KEY_DOWN):
            self.log_scroll = min(self.log_scroll + 1, max(0, len(self.log_lines) - h))
        elif key in (ord("k"), curses.KEY_UP):
            self.log_scroll = max(0, self.log_scroll - 1)
        elif key in (ord("G"),):
            self.log_scroll = max(0, len(self.log_lines) - h)
        elif key in (ord("g"),):
            self.log_scroll = 0

    def handle_main_input(self, key):
        with self.lock:
            items = self.filtered()
        n = len(items)

        if key in (ord("q"),):
            self.running = False
        elif key in (ord("j"), curses.KEY_DOWN):
            self.cursor = min(self.cursor + 1, n - 1) if n else 0
        elif key in (ord("k"), curses.KEY_UP):
            self.cursor = max(self.cursor - 1, 0)
        elif key == ord("g"):
            self.cursor = 0
        elif key == ord("G"):
            self.cursor = max(0, n - 1)
        elif key == ord("{"):
            self.cursor = max(0, self.cursor - 10)
        elif key == ord("}"):
            self.cursor = min(max(0, n - 1), self.cursor + 10)
        elif key == ord("f"):
            modes = ["all", "running", "stopped"]
            self.filter_mode = modes[(modes.index(self.filter_mode) + 1) % 3]
            self.cursor = 0
        elif key == ord("/"):
            self.prompt_search()
            self.cursor = 0
        elif key == ord("l"):
            self.open_logs()
        else:
            c = self.get_selected()
            if not c:
                return
            if key in (10, ord("s")):  # Enter or s
                self.do_action("start", c["id"], c["name"])
            elif key == ord("S"):
                self.do_action("stop", c["id"], c["name"])
            elif key == ord("r"):
                self.do_action("restart", c["id"], c["name"])
            elif key == ord("p"):
                act = "unpause" if "paused" in c["status"].lower() else "pause"
                self.do_action(act, c["id"], c["name"])
            elif key == ord("d"):
                if self.confirm(f"Remove {c['name']}?"):
                    self.do_action("rm", c["id"], c["name"])

    def run(self):
        self.scr.nodelay(False)
        self.scr.timeout(300)
        curses.curs_set(0)
        setup_colors()

        t = threading.Thread(target=self.refresh_data, daemon=True)
        t.start()
        # Initial fetch so we don't start with empty screen
        self.containers = fetch_containers()

        while self.running:
            self.scr.erase()
            if self.view == "logs":
                self.draw_logs()
            else:
                self.draw_header()
                self.draw_table()
                self.draw_statusbar()
            self.scr.refresh()

            key = self.scr.getch()
            if key == -1:
                continue
            if self.view == "logs":
                self.handle_logs_input(key)
            else:
                self.handle_main_input(key)


def main(stdscr):
    if not docker_available():
        stdscr.addstr(0, 0, "Docker is not available. Is it installed and running?")
        stdscr.refresh()
        stdscr.getch()
        return
    DockerManager(stdscr).run()


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