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

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


APP_BG = "#F3F6FB"
HEADER_BG = "#001A66"
CARD_BG = "#2C3445"
CARD_BORDER = "#1F2533"
TABLE_BG = "#2C3445"
TABLE_HEADER_BG = "#3E6FE0"
TEXT_LIGHT = "#F7F9FD"
TEXT_MUTED = "#C7D0E3"
TEXT_DARK = "#fff"
SUCCESS = "#35C97A"
WARNING = "#F5A047"
CRITICAL = "#FF5A67"
CONFIG_FILE_NAME = "ops_monitor_config.json"
ENV_FILE_NAME = ".env"


def base_dir() -> Path:
    if getattr(__import__("sys"), "frozen", False):  # pragma: no cover
        return Path(__import__("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)
        value = value.strip().strip('"').strip("'")
        if value.lower() == "null":
            value = ""
        data[key.strip()] = value
    return data


def load_saved_config() -> dict:
    file_path = config_path()
    if not file_path.exists():
        return {}
    try:
        return json.loads(file_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")


class SnapshotPoller(threading.Thread):
    def __init__(self, client_factory, config: dict, refresh_ms: int, outbox):
        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 shutdown(self):
        self.stop_event.set()

    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


class OpsMonitorApp:
    def __init__(self, root: tk.Tk, 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.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"),
        }

        self.root.title("TNBT Server Monitor")
        self.root.geometry("1360x780")
        self.root.minsize(980, 600)
        self.root.configure(bg=APP_BG)
        self.root.protocol("WM_DELETE_WINDOW", self.on_close)

        self.setup_style()
        self.build_layout()

        self.poller = None
        self.restart_poller(show_message=False)
        self.process_queue()

    def setup_style(self):
        style = ttk.Style()
        style.theme_use("clam")
        style.configure("Header.TFrame", background=HEADER_BG)
        style.configure("Main.TFrame", background=APP_BG)
        style.configure(
            "Dark.Treeview",
            background=TABLE_BG,
            fieldbackground=TABLE_BG,
            foreground=TEXT_LIGHT,
            borderwidth=0,
            rowheight=52,
            font=("Segoe UI", 11),
        )
        style.map("Dark.Treeview", background=[("selected", "#2358F5")])
        style.configure(
            "Dark.Treeview.Heading",
            background=TABLE_HEADER_BG,
            foreground=TEXT_LIGHT,
            borderwidth=0,
            relief="flat",
            padding=(12, 10),
            font=("Segoe UI Semibold", 11),
        )
        style.configure("Vertical.TScrollbar", troughcolor=CARD_BG, background="#6B7EA8", borderwidth=0, arrowsize=14)

    def build_layout(self):
        container = ttk.Frame(self.root, style="Main.TFrame")
        container.pack(fill="both", expand=True)
        self.build_header(container)
        self.build_summary(container)
        self.build_table(container)

    def build_header(self, parent):
        header = ttk.Frame(parent, style="Header.TFrame", padding=(22, 18, 22, 18))
        header.pack(fill="x")

        left = tk.Frame(header, bg=HEADER_BG)
        left.pack(side="left", fill="y")

        if self.logo_path and os.path.exists(self.logo_path):
            try:
                self.logo_image = tk.PhotoImage(file=self.logo_path)
                tk.Label(left, image=self.logo_image, bg=HEADER_BG).pack(anchor="w")
            except Exception:
                self.build_text_logo(left)
        else:
            self.build_text_logo(left)

        right = tk.Frame(header, bg=HEADER_BG)
        right.pack(side="right", fill="y")

        settings_button = tk.Button(
            right,
            text="Redis Settings",
            command=self.open_settings_dialog,
            bg=HEADER_BG,
            fg=TEXT_LIGHT,
            activebackground=HEADER_BG,
            activeforeground=TEXT_LIGHT,
            relief="flat",
            font=("Segoe UI Semibold", 11),
            cursor="hand2",
            padx=8,
            pady=8,
        )
        settings_button.pack(anchor="e", pady=(0, 8))

        chip = tk.Frame(right, bg=HEADER_BG, highlightbackground="#FFFFFF", highlightthickness=2)
        chip.pack(anchor="e", padx=(10, 0), pady=(6, 0))

        tk.Label(
            chip,
            textvariable=self.status_var,
            bg=HEADER_BG,
            fg=TEXT_LIGHT,
            font=("Segoe UI", 12),
            padx=10,
            pady=10,
        ).pack()

    def build_text_logo(self, parent):
        tk.Label(
            parent,
            text="TNBT",
            bg=HEADER_BG,
            fg=TEXT_DARK,
            font=("Segoe UI Black", 36),
        ).pack(anchor="w")

    def build_summary(self, parent):
        wrap = tk.Frame(parent, bg=APP_BG)
        wrap.pack(fill="x", padx=2, pady=3)

        cards = [
            ("Active Users", "active_users"),
            ("Web Users", "web_users"),
            ("App Users", "app_users"),
            ("Total Requests", "requests_total"),
            ("Last Update", "updated_at"),
            ("Health", "health"),
        ]

        self.summary_cards = []
        for index, (label, key) in enumerate(cards):
            card = tk.Frame(
                wrap,
                bg=CARD_BG,
                highlightbackground=CARD_BORDER,
                highlightthickness=0,
                padx=8,
                pady=8,
            )
            card.grid(row=0, column=index, sticky="nsew", padx=2, pady=2)
            wrap.grid_columnconfigure(index, weight=1)

            tk.Label(card, text=label, bg=CARD_BG, fg=TEXT_MUTED, font=("Segoe UI Semibold", 10)).pack(anchor="w")
            value_font = ("Segoe UI Semibold", 18 if key != "updated_at" else 12)
            tk.Label(card, textvariable=self.summary_vars[key], bg=CARD_BG, fg=TEXT_LIGHT, font=value_font).pack(anchor="w", pady=(0, 0))
            self.summary_cards.append(card)

    def build_table(self, parent):
        outer = tk.Frame(parent, bg=APP_BG)
        outer.pack(fill="both", expand=True, padx=2, pady=2)

        shell = tk.Frame(outer, bg=TABLE_BG, highlightbackground=CARD_BORDER, highlightthickness=1)
        shell.pack(fill="both", expand=True)

        columns = (
            "project_name",
            "user_name",
            "status",
            "priority",
            "progress",
            "due_date",
            "email",
            "device",
            "route",
            "timeing",
        )

        self.tree = ttk.Treeview(shell, columns=columns, show="headings", style="Dark.Treeview")
        headings = {
            "project_name": "PROJECT NAME",
            "user_name": "ASSIGNEE",
            "status": "STATUS",
            "priority": "PRIORITY",
            "progress": "PROGRESS",
            "due_date": "DUE DATE",
            "email": "EMAIL ID",
            "device": "DEVICE",
            "route": "CURRENT ROUTE / API",
            "timeing": "TIMEING",
        }
        widths = {
            "project_name": 220,
            "user_name": 180,
            "status": 140,
            "priority": 120,
            "progress": 120,
            "due_date": 150,
            "email": 200,
            "device": 100,
            "route": 280,
            "timeing": 100,
        }

        for column in columns:
            self.tree.heading(column, text=headings[column])
            self.tree.column(column, width=widths[column], minwidth=10, stretch=True, anchor="w")

        scrollbar = ttk.Scrollbar(shell, orient="vertical", command=self.tree.yview)
        self.tree.configure(yscrollcommand=scrollbar.set)
        self.tree.pack(side="left", fill="both", expand=True)
        scrollbar.pack(side="right", fill="y")

    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.set_health_style("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 to Redis  |  {self.redis_config.get('host')}:{self.redis_config.get('port')}  |  db={self.redis_config.get('db')}"
            )

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

        for item in self.tree.get_children():
            self.tree.delete(item)

        active_web = 0
        active_app = 0
        max_timing = 0.0
        latest_update = payload.get("updated_at") or "-"

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

            timing = float(row.get("timing_ms", 0) or 0)
            max_timing = max(max_timing, timing)
            status = self.format_status(timing, int(row.get("status_code", 200) or 200))
            priority = self.format_priority(row.get("network_status", ""))
            progress = self.format_progress(timing)

            self.tree.insert(
                "",
                "end",
                values=(
                    row.get("current_route", "/"),
                    row.get("name", "Unknown User"),
                    status,
                    priority,
                    progress,
                    row.get("last_seen_at", "-"),
                    row.get("email", ""),
                    row.get("device_info", ""),
                    row.get("current_route", ""),
                    f"{timing:.0f} ms",
                ),
            )

        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}  |  total_requests={total_requests}")
        self.set_health_style(health)

    def format_status(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 format_priority(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 format_progress(self, timing_ms: float) -> str:
        if timing_ms >= 1500:
            return "15%"
        if timing_ms >= 1000:
            return "35%"
        if timing_ms >= 600:
            return "60%"
        if timing_ms >= 300:
            return "80%"
        return "100%"

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

    def set_health_style(self, health: str):
        color = SUCCESS
        if health == "WARNING":
            color = WARNING
        elif health == "CRITICAL":
            color = CRITICAL
        self.summary_cards[-1].configure(highlightbackground=color)

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

    def open_settings_dialog(self):
        dialog = tk.Toplevel(self.root)
        dialog.title("Settings")
        dialog.geometry("600x650")
        dialog.resizable(False, False)
        dialog.configure(bg=APP_BG)
        dialog.transient(self.root)
        dialog.grab_set()

        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"))),
        }

        card = tk.Frame(dialog, bg="#2C3445", highlightbackground=CARD_BORDER, highlightthickness=1, padx=18, pady=18)
        card.pack(fill="both", expand=True, padx=0, pady=0)

        tk.Label(card, text="Connection Settings", bg=CARD_BG, fg=TEXT_LIGHT, font=("Segoe UI Semibold", 15)).pack(anchor="w", pady=(0, 12))

        fields = [
            ("REDIS_CLIENT", "client"),
            ("REDIS_HOST", "host"),
            ("REDIS_PORT", "port"),
            ("REDIS_DB", "db"),
            ("REDIS_PASSWORD", "password"),
            ("OPS_MONITOR_REDIS_KEY", "key"),
        ]

        for label_text, key in fields:
            tk.Label(card, text=label_text, bg=CARD_BG, fg=TEXT_MUTED, font=("Segoe UI", 10)).pack(anchor="w", pady=(8, 4))
            entry = tk.Entry(card, textvariable=values[key], bg="#202736", fg=TEXT_LIGHT, insertbackground=TEXT_LIGHT, relief="flat", font=("Segoe UI", 11))
            entry.pack(fill="x", ipady=8)

        button_row = tk.Frame(card, bg=CARD_BG)
        button_row.pack(fill="x", pady=(18, 0))

        def save_and_reconnect():
            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()

        tk.Button(
            button_row,
            text="Cancel",
            command=dialog.destroy,
            bg="#3A4357",
            fg=TEXT_LIGHT,
            relief="flat",
            font=("Segoe UI Semibold", 10),
            padx=18,
            pady=10,
            cursor="hand2",
        ).pack(side="right", padx=(10, 0))

        tk.Button(
            button_row,
            text="Save & Connect",
            command=save_and_reconnect,
            bg=HEADER_BG,
            fg=TEXT_LIGHT,
            relief="flat",
            font=("Segoe UI Semibold", 10),
            padx=18,
            pady=10,
            cursor="hand2",
        ).pack(side="right")


def parse_args():
    parser = argparse.ArgumentParser(description="TNBT-style Redis server monitor with non-blocking live 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 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,
    )


def resolve_runtime_config(args) -> dict:
    env_values = parse_env_file(env_path())
    saved = load_saved_config()

    config = {
        "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,
    }

    return config


def main():
    args = parse_args()
    if redis is None:
        raise SystemExit("Missing dependency: pip install redis")

    runtime_config = resolve_runtime_config(args)

    root = tk.Tk()
    OpsMonitorApp(root, runtime_config, args.refresh_ms, args.logo_path)
    root.mainloop()


if __name__ == "__main__":
    main()
