import argparse
import json
import os
import queue
import sys
import threading
import tkinter as tk
from pathlib import Path

import customtkinter as ctk

try:
    from PIL import Image
except ImportError:  # pragma: no cover
    Image = None

try:
    import redis
except ImportError:  # pragma: no cover
    redis = None


HEADER_BG = "#3157C6"
LIGHT_BG = "#EEF3FB"
LIGHT_CARD = "#FFFFFF"
LIGHT_BORDER = "#DCE6F7"
LIGHT_TEXT = "#101522"
LIGHT_MUTED = "#60708E"
LIGHT_TABLE = "#F6F9FF"
LIGHT_ROW = "#FFFFFF"
DARK_BG = "#161D2E"
DARK_CARD = "#242C3E"
DARK_BORDER = "#33405B"
DARK_TEXT = "#F7F9FD"
DARK_MUTED = "#A9B8D3"
DARK_TABLE = "#1E2638"
DARK_ROW = "#283248"
SUCCESS = "#2CBF6E"
WARNING = "#F29A38"
CRITICAL = "#EF5A6A"
PROGRESS = "#5CA1FF"
CONFIG_FILE_NAME = "ops_monitor_config.json"
ENV_FILE_NAME = ".env"


def base_dir() -> Path:
    if getattr(sys, "frozen", False):  # pragma: no cover
        return Path(sys.executable).resolve().parent
    return Path(__file__).resolve().parent


def config_path() -> Path:
    return base_dir() / CONFIG_FILE_NAME


def env_path() -> Path:
    return base_dir() / ENV_FILE_NAME


def parse_env_file(path: Path) -> dict:
    if not path.exists():
        return {}

    data = {}
    for raw_line in path.read_text(encoding="utf-8").splitlines():
        line = raw_line.strip()
        if not line or line.startswith("#") or "=" not in line:
            continue
        key, value = line.split("=", 1)
        cleaned = value.strip().strip('"').strip("'")
        if cleaned.lower() == "null":
            cleaned = ""
        data[key.strip()] = cleaned
    return data


def load_saved_config() -> dict:
    path = config_path()
    if not path.exists():
        return {}
    try:
        return json.loads(path.read_text(encoding="utf-8"))
    except Exception:
        return {}


def save_config(data: dict):
    config_path().write_text(json.dumps(data, indent=2), encoding="utf-8")


def build_redis_client(config: dict):
    return redis.Redis(
        host=config["host"],
        port=int(config["port"]),
        db=int(config["db"]),
        password=config.get("password") or None,
        decode_responses=True,
        socket_timeout=2,
        socket_connect_timeout=2,
        retry_on_timeout=True,
        health_check_interval=15,
    )


class SnapshotPoller(threading.Thread):
    def __init__(self, client_factory, config: dict, refresh_ms: int, outbox: queue.Queue):
        super().__init__(daemon=True)
        self.client_factory = client_factory
        self.config = config
        self.refresh_ms = refresh_ms
        self.outbox = outbox
        self.stop_event = threading.Event()

    def run(self):
        client = self.client_factory(self.config)
        while not self.stop_event.is_set():
            try:
                raw = client.get(self.config["key"])
                payload = json.loads(raw) if raw else {"users": {}, "updated_at": None, "requests_total": 0}
                self.publish(("snapshot", payload))
            except Exception as exc:  # pragma: no cover
                self.publish(("error", str(exc)))
            self.stop_event.wait(self.refresh_ms / 1000)

    def publish(self, item):
        try:
            while self.outbox.qsize() > 1:
                self.outbox.get_nowait()
            self.outbox.put_nowait(item)
        except queue.Full:  # pragma: no cover
            pass

    def shutdown(self):
        self.stop_event.set()


class MonitorRow(ctk.CTkFrame):
    COLUMN_WEIGHTS = (2, 1.2, 1, 1, 1, 1.1, 1.8, 1, 2.2, 0.9)

    def __init__(self, parent, palette: dict):
        super().__init__(parent, fg_color=palette["row"], corner_radius=16, border_width=1, border_color=palette["border"])
        self.palette = palette
        self.widgets = []
        for index, weight in enumerate(self.COLUMN_WEIGHTS):
            self.grid_columnconfigure(index, weight=weight)

    def apply_palette(self, palette: dict):
        self.palette = palette
        self.configure(fg_color=palette["row"], border_color=palette["border"])
        for widget in self.widgets:
            role = getattr(widget, "_role", "")
            if role == "text":
                widget.configure(text_color=palette["text"])
            elif role == "muted":
                widget.configure(text_color=palette["muted"])
            elif role == "track":
                widget.configure(fg_color=palette["track"])

    def render(self, row: dict, palette: dict):
        self.apply_palette(palette)
        for child in self.winfo_children():
            child.destroy()
        self.widgets = []

        timing = float(row.get("timing_ms", 0) or 0)
        status = self.status_label(timing, int(row.get("status_code", 200) or 200))
        priority = self.priority_label(row.get("network_status", ""))
        progress = self.progress_value(timing)

        values = [
            ("text", row.get("current_route", "/")),
            ("text", row.get("name", "Unknown User")),
            ("badge", status),
            ("muted", priority),
            ("progress", progress),
            ("muted", row.get("last_seen_at", "-")),
            ("muted", row.get("email", "")),
            ("muted", row.get("device_info", "")),
            ("muted", row.get("current_route", "")),
            ("text", f"{timing:.0f} ms"),
        ]

        for index, (kind, value) in enumerate(values):
            if kind == "badge":
                badge = ctk.CTkLabel(
                    self,
                    text=value,
                    fg_color=self.status_color(value),
                    text_color="#FFFFFF",
                    corner_radius=999,
                    font=ctk.CTkFont(size=12, weight="bold"),
                    padx=14,
                    pady=8,
                )
                badge.grid(row=0, column=index, padx=10, pady=10, sticky="w")
                self.widgets.append(badge)
            elif kind == "progress":
                track = ctk.CTkFrame(self, fg_color=palette["track"], corner_radius=999, height=10)
                track._role = "track"
                track.grid(row=0, column=index, padx=10, pady=12, sticky="ew")
                fill = ctk.CTkFrame(track, fg_color=PROGRESS, corner_radius=999, height=10)
                fill.place(relx=0, rely=0, relwidth=value, relheight=1)
                self.widgets.append(track)
            else:
                label = ctk.CTkLabel(
                    self,
                    text=value,
                    text_color=palette["text"] if kind == "text" else palette["muted"],
                    fg_color="transparent",
                    font=ctk.CTkFont(size=13, weight="bold" if kind == "text" else "normal"),
                    anchor="w",
                    justify="left",
                )
                label._role = kind
                label.grid(row=0, column=index, padx=10, pady=12, sticky="ew")
                self.widgets.append(label)

    def status_label(self, timing_ms: float, status_code: int) -> str:
        if status_code >= 500:
            return "CRITICAL"
        if timing_ms >= 1000:
            return "PENDING"
        if timing_ms >= 400:
            return "IN PROGRESS"
        return "COMPLETED"

    def priority_label(self, network_status: str) -> str:
        value = str(network_status or "").strip().lower()
        if value == "weak":
            return "High"
        if value == "normal":
            return "Medium"
        return "Low"

    def progress_value(self, timing_ms: float) -> float:
        if timing_ms >= 1500:
            return 0.15
        if timing_ms >= 1000:
            return 0.35
        if timing_ms >= 600:
            return 0.60
        if timing_ms >= 300:
            return 0.80
        return 1.0

    def status_color(self, status: str) -> str:
        if status == "CRITICAL":
            return CRITICAL
        if status == "PENDING":
            return WARNING
        if status == "IN PROGRESS":
            return "#3B82F6"
        return SUCCESS


class TNBTMonitorApp:
    HEADER_COLUMNS = [
        "PROJECT NAME",
        "ASSIGNEE",
        "STATUS",
        "PRIORITY",
        "PROGRESS",
        "LAST UPDATE",
        "EMAIL ID",
        "DEVICE",
        "CURRENT ROUTE / API",
        "TIMEING",
    ]

    def __init__(self, root: ctk.CTk, redis_config: dict, refresh_ms: int, logo_path: str | None):
        self.root = root
        self.redis_config = redis_config
        self.refresh_ms = refresh_ms
        self.logo_path = logo_path
        self.logo_image = None
        self.queue = queue.Queue(maxsize=4)
        self.poller = None
        self.rows = []
        self.is_dark = False

        self.status_var = tk.StringVar(value="Connecting to Redis...")
        self.summary_vars = {
            "active_users": tk.StringVar(value="0"),
            "web_users": tk.StringVar(value="0"),
            "app_users": tk.StringVar(value="0"),
            "requests_total": tk.StringVar(value="0"),
            "updated_at": tk.StringVar(value="-"),
            "health": tk.StringVar(value="WAITING"),
        }

        ctk.set_appearance_mode("light")
        self.root.title("TNBT Server Monitor")
        self.root.geometry("1460x820")
        self.root.minsize(1080, 620)
        self.root.protocol("WM_DELETE_WINDOW", self.on_close)

        self.build_layout()
        self.apply_theme()
        self.restart_poller(show_message=False)
        self.process_queue()

    def palette(self) -> dict:
        if self.is_dark:
            return {
                "app": DARK_BG,
                "card": DARK_CARD,
                "row": DARK_ROW,
                "border": DARK_BORDER,
                "text": DARK_TEXT,
                "muted": DARK_MUTED,
                "table": DARK_TABLE,
                "track": "#39445F",
            }
        return {
            "app": LIGHT_BG,
            "card": LIGHT_CARD,
            "row": LIGHT_ROW,
            "border": LIGHT_BORDER,
            "text": LIGHT_TEXT,
            "muted": LIGHT_MUTED,
            "table": LIGHT_TABLE,
            "track": "#D8E3F7",
        }

    def build_layout(self):
        palette = self.palette()
        self.main_shell = ctk.CTkFrame(self.root, fg_color=palette["app"], corner_radius=0)
        self.main_shell.pack(fill="both", expand=True)

        header = ctk.CTkFrame(self.main_shell, fg_color=HEADER_BG, corner_radius=0, height=106)
        header.pack(fill="x")
        header.pack_propagate(False)

        header_left = ctk.CTkFrame(header, fg_color="transparent")
        header_left.pack(side="left", padx=28, pady=18)

        if self.logo_path and os.path.exists(self.logo_path) and Image is not None:
            try:
                image = Image.open(self.logo_path)
                self.logo_image = ctk.CTkImage(light_image=image, dark_image=image, size=(180, 54))
            except Exception:
                self.logo_image = None

        if self.logo_image:
            self.logo_label = ctk.CTkLabel(header_left, text="", image=self.logo_image)
        else:
            self.logo_label = ctk.CTkLabel(header_left, text="TNBT", font=ctk.CTkFont(size=46, weight="bold"))
        self.logo_label.pack(anchor="w")

        header_right = ctk.CTkFrame(header, fg_color="transparent")
        header_right.pack(side="right", padx=22, pady=18)

        button_row = ctk.CTkFrame(header_right, fg_color="transparent")
        button_row.pack(anchor="e")

        self.toggle_button = ctk.CTkButton(button_row, text="Dark Mode", width=120, height=42, corner_radius=24, command=self.toggle_mode)
        self.toggle_button.pack(side="left", padx=(0, 12))

        self.settings_button = ctk.CTkButton(
            button_row,
            text="Redis Settings",
            width=150,
            height=42,
            corner_radius=24,
            border_width=2,
            fg_color="transparent",
            command=self.open_settings_dialog,
        )
        self.settings_button.pack(side="left")

        self.status_chip = ctk.CTkFrame(header_right, fg_color="transparent", corner_radius=24, border_width=2, width=380, height=44)
        self.status_chip.pack(anchor="e", pady=(12, 0))
        self.status_chip.pack_propagate(False)

        self.status_label = ctk.CTkLabel(self.status_chip, textvariable=self.status_var, font=ctk.CTkFont(size=14, weight="medium"))
        self.status_label.pack(fill="both", expand=True, padx=14)

        self.summary_wrap = ctk.CTkFrame(self.main_shell, fg_color=palette["app"], corner_radius=0)
        self.summary_wrap.pack(fill="x", padx=18, pady=(16, 12))
        for index in range(6):
            self.summary_wrap.grid_columnconfigure(index, weight=1)

        self.summary_cards = {}
        summary_items = [
            ("Active Users", "active_users"),
            ("Web Users", "web_users"),
            ("App Users", "app_users"),
            ("Total Requests", "requests_total"),
            ("Last Update", "updated_at"),
            ("Health", "health"),
        ]
        for index, (title, key) in enumerate(summary_items):
            card = ctk.CTkFrame(self.summary_wrap, corner_radius=20, border_width=1)
            card.grid(row=0, column=index, padx=8, pady=8, sticky="nsew")
            title_label = ctk.CTkLabel(card, text=title, anchor="w", font=ctk.CTkFont(size=12, weight="bold"))
            title_label.pack(anchor="w", padx=16, pady=(14, 4))
            value_label = ctk.CTkLabel(card, textvariable=self.summary_vars[key], anchor="w", font=ctk.CTkFont(size=20 if key != "updated_at" else 13, weight="bold"))
            value_label.pack(anchor="w", padx=16, pady=(0, 14))
            self.summary_cards[key] = {"frame": card, "title": title_label, "value": value_label}

        self.table_shell = ctk.CTkFrame(self.main_shell, corner_radius=24, border_width=1)
        self.table_shell.pack(fill="both", expand=True, padx=18, pady=(0, 18))

        self.table_header = ctk.CTkFrame(self.table_shell, fg_color=HEADER_BG, corner_radius=18)
        self.table_header.pack(fill="x", padx=10, pady=10)
        for index, weight in enumerate(MonitorRow.COLUMN_WEIGHTS):
            self.table_header.grid_columnconfigure(index, weight=weight)

        self.header_labels = []
        for index, title in enumerate(self.HEADER_COLUMNS):
            label = ctk.CTkLabel(self.table_header, text=title, text_color="#FFFFFF", font=ctk.CTkFont(size=12, weight="bold"), anchor="w")
            label.grid(row=0, column=index, padx=10, pady=16, sticky="w")
            self.header_labels.append(label)

        self.table_scroll = ctk.CTkScrollableFrame(self.table_shell, corner_radius=18)
        self.table_scroll.pack(fill="both", expand=True, padx=10, pady=(0, 10))

    def apply_theme(self):
        palette = self.palette()
        self.root.configure(fg_color=palette["app"])
        self.main_shell.configure(fg_color=palette["app"])
        self.summary_wrap.configure(fg_color=palette["app"])
        self.table_shell.configure(fg_color=palette["card"], border_color=palette["border"])
        self.table_scroll.configure(fg_color=palette["table"])
        self.logo_label.configure(text_color="#11151F" if not self.is_dark else "#EAF0FD")
        self.toggle_button.configure(
            text="Dark Mode" if not self.is_dark else "Light Mode",
            fg_color="#FFFFFF" if not self.is_dark else "#27324B",
            text_color="#18326E" if not self.is_dark else "#EAF0FD",
            hover_color="#E9F0FF" if not self.is_dark else "#344262",
        )
        self.settings_button.configure(border_color="#FFFFFF", text_color="#FFFFFF", hover_color="#3B63D3")
        self.status_chip.configure(border_color="#FFFFFF")
        self.status_label.configure(text_color="#FFFFFF")

        for card in self.summary_cards.values():
            card["frame"].configure(fg_color=palette["card"], border_color=palette["border"])
            card["title"].configure(text_color=palette["muted"])
            card["value"].configure(text_color=palette["text"])

        for row in self.rows:
            row.apply_palette(palette)

    def toggle_mode(self):
        self.is_dark = not self.is_dark
        ctk.set_appearance_mode("dark" if self.is_dark else "light")
        self.apply_theme()

    def process_queue(self):
        try:
            while True:
                kind, payload = self.queue.get_nowait()
                if kind == "snapshot":
                    self.render_snapshot(payload)
                else:
                    self.status_var.set(f"Redis monitor error | {payload}")
                    self.summary_vars["health"].set("CRITICAL")
                    self.update_health_border("CRITICAL")
        except queue.Empty:
            pass
        self.root.after(180, self.process_queue)

    def restart_poller(self, show_message=True):
        if self.poller is not None:
            self.poller.shutdown()
        self.poller = SnapshotPoller(build_redis_client, self.redis_config.copy(), self.refresh_ms, self.queue)
        self.poller.start()
        if show_message:
            self.status_var.set(f"Reconnecting | {self.redis_config.get('host')}:{self.redis_config.get('port')} | db={self.redis_config.get('db')}")

    def clear_rows(self):
        for row in self.rows:
            row.destroy()
        self.rows = []

    def render_snapshot(self, payload: dict):
        palette = self.palette()
        users = payload.get("users", {})
        rows = sorted(users.values(), key=lambda item: item.get("last_seen_at", ""), reverse=True)
        self.clear_rows()

        active_web = 0
        active_app = 0
        max_timing = 0.0

        for row_data in rows:
            row = MonitorRow(self.table_scroll, palette)
            row.pack(fill="x", padx=6, pady=6)
            row.render(row_data, palette)
            self.rows.append(row)

            user_type = str(row_data.get("userstype", "")).strip().lower()
            if user_type == "web":
                active_web += 1
            elif user_type == "app":
                active_app += 1

            timing = float(row_data.get("timing_ms", 0) or 0)
            max_timing = max(max_timing, timing)

        latest_update = payload.get("updated_at") or "-"
        total_requests = int(payload.get("requests_total", 0) or 0)
        health = self.health_label(max_timing)

        self.summary_vars["active_users"].set(str(len(rows)))
        self.summary_vars["web_users"].set(str(active_web))
        self.summary_vars["app_users"].set(str(active_app))
        self.summary_vars["requests_total"].set(str(total_requests))
        self.summary_vars["updated_at"].set(latest_update)
        self.summary_vars["health"].set(health)
        self.status_var.set(f"Live monitor running | updated_at={latest_update} | requests={total_requests}")
        self.update_health_border(health)

    def health_label(self, max_timing: float) -> str:
        if max_timing >= 1500:
            return "CRITICAL"
        if max_timing >= 700:
            return "WARNING"
        return "HEALTHY"

    def update_health_border(self, health: str):
        color = SUCCESS
        if health == "WARNING":
            color = WARNING
        elif health == "CRITICAL":
            color = CRITICAL
        self.summary_cards["health"]["frame"].configure(border_color=color)

    def open_settings_dialog(self):
        dialog = ctk.CTkToplevel(self.root)
        dialog.title("Redis Settings")
        dialog.geometry("520x470")
        dialog.resizable(False, False)
        dialog.transient(self.root)
        dialog.grab_set()

        palette = self.palette()
        dialog.configure(fg_color=palette["app"])
        card = ctk.CTkFrame(dialog, fg_color=palette["card"], corner_radius=22, border_width=1, border_color=palette["border"])
        card.pack(fill="both", expand=True, padx=18, pady=18)

        ctk.CTkLabel(card, text="Redis Connection Settings", text_color=palette["text"], font=ctk.CTkFont(size=18, weight="bold")).pack(
            anchor="w", padx=18, pady=(18, 10)
        )

        values = {
            "client": tk.StringVar(value=str(self.redis_config.get("client", "redis"))),
            "host": tk.StringVar(value=str(self.redis_config.get("host", "127.0.0.1"))),
            "port": tk.StringVar(value=str(self.redis_config.get("port", "6379"))),
            "db": tk.StringVar(value=str(self.redis_config.get("db", "0"))),
            "password": tk.StringVar(value="" if self.redis_config.get("password") in (None, "null") else str(self.redis_config.get("password", ""))),
            "key": tk.StringVar(value=str(self.redis_config.get("key", "rsc:ops:live_monitor"))),
        }

        for label_text, key in [
            ("REDIS_CLIENT", "client"),
            ("REDIS_HOST", "host"),
            ("REDIS_PORT", "port"),
            ("REDIS_DB", "db"),
            ("REDIS_PASSWORD", "password"),
            ("OPS_MONITOR_REDIS_KEY", "key"),
        ]:
            ctk.CTkLabel(card, text=label_text, text_color=palette["muted"], font=ctk.CTkFont(size=12, weight="bold")).pack(
                anchor="w", padx=18, pady=(8, 4)
            )
            ctk.CTkEntry(card, textvariable=values[key], height=40, corner_radius=14).pack(fill="x", padx=18)

        buttons = ctk.CTkFrame(card, fg_color="transparent")
        buttons.pack(fill="x", padx=18, pady=18)

        def save_and_connect():
            next_config = {
                "client": values["client"].get().strip() or "redis",
                "host": values["host"].get().strip() or "127.0.0.1",
                "port": int(values["port"].get().strip() or "6379"),
                "db": int(values["db"].get().strip() or "0"),
                "password": values["password"].get().strip(),
                "key": values["key"].get().strip() or "rsc:ops:live_monitor",
            }
            self.redis_config = next_config
            save_config(next_config)
            self.restart_poller()
            dialog.destroy()

        ctk.CTkButton(buttons, text="Cancel", width=120, height=42, corner_radius=18, fg_color="#6B7896", command=dialog.destroy).pack(
            side="right", padx=(12, 0)
        )
        ctk.CTkButton(buttons, text="Save & Connect", width=140, height=42, corner_radius=18, fg_color=HEADER_BG, command=save_and_connect).pack(
            side="right"
        )

    def on_close(self):
        if self.poller is not None:
            self.poller.shutdown()
        self.root.after(120, self.root.destroy)


def parse_args():
    parser = argparse.ArgumentParser(description="TNBT Redis monitor UI")
    parser.add_argument("--client", default=os.getenv("REDIS_CLIENT", "redis"))
    parser.add_argument("--host", default=os.getenv("REDIS_HOST", "127.0.0.1"))
    parser.add_argument("--port", type=int, default=int(os.getenv("REDIS_PORT", "6379")))
    parser.add_argument("--db", type=int, default=int(os.getenv("REDIS_DB", "0")))
    parser.add_argument("--password", default=os.getenv("REDIS_PASSWORD"))
    parser.add_argument("--key", default=os.getenv("OPS_MONITOR_REDIS_KEY", "rsc:ops:live_monitor"))
    parser.add_argument("--refresh-ms", type=int, default=2000)
    parser.add_argument("--logo-path", default=os.getenv("OPS_MONITOR_LOGO"))
    return parser.parse_args()


def resolve_runtime_config(args) -> dict:
    env_values = parse_env_file(env_path())
    saved = load_saved_config()
    return {
        "client": saved.get("client") or env_values.get("REDIS_CLIENT") or args.client,
        "host": saved.get("host") or env_values.get("REDIS_HOST") or args.host,
        "port": saved.get("port") or env_values.get("REDIS_PORT") or args.port,
        "db": saved.get("db") or env_values.get("REDIS_DB") or args.db,
        "password": saved.get("password") if "password" in saved else env_values.get("REDIS_PASSWORD", args.password),
        "key": saved.get("key") or env_values.get("OPS_MONITOR_REDIS_KEY") or args.key,
    }


def main():
    if redis is None:
        raise SystemExit("Missing dependency: pip install redis customtkinter pillow")

    args = parse_args()
    runtime_config = resolve_runtime_config(args)
    root = ctk.CTk()
    TNBTMonitorApp(root, runtime_config, args.refresh_ms, args.logo_path)
    root.mainloop()


if __name__ == "__main__":
    main()
