"""Optional Tkinter GUI for GenWorker.

Launch with:
    genworker --gui
    python -m genworker --gui

The GUI is optional — the primary interface remains the CLI.
Tkinter is part of Python's standard library, so no extra dependencies are needed.
"""

from __future__ import annotations

import io
import queue
import sys
import threading
import tkinter as tk
from tkinter import scrolledtext, ttk

from PIL import Image, ImageDraw, ImageTk

from genworker.config import DEFAULT_MONITOR, MAX_STEPS, TASK_TIMEOUT, SCREENSHOT_GRID_OVERLAY, IS_MACOS
from genworker.tools.computer import get_monitor, list_monitors
import genworker.tools.computer as _computer_mod


# ─── Theme Colours ────────────────────────────────────────────────────────────

_BG       = "#2b2b2b"   # main background
_BG_DARK  = "#1e1e1e"   # deep background (log, input)
_BG_LIGHT = "#3c3c3c"   # elevated surfaces
_FG       = "#e0e0e0"   # primary text
_FG_DIM   = "#888888"   # secondary text
_GREEN    = "#2e7d32"    # primary action
_GREEN_HI = "#388e3c"    # hover
_RED      = "#c62828"    # destructive action
_RED_HI   = "#d32f2f"    # hover


# ─── Stdout Redirector ─────────────────────────────────────────────────────────

class _QueueWriter(io.TextIOBase):
    """Redirect print() output into a thread-safe queue."""

    def __init__(self, q: queue.Queue):
        self._queue = q

    def write(self, text: str) -> int:
        if text:
            self._queue.put(("log", text))
        return len(text)

    def flush(self):
        pass


# ─── Main GUI Window ───────────────────────────────────────────────────────────

class GenWorkerGUI:
    """Tkinter GUI for running GenWorker tasks.

    Runs the agent in a background thread so the UI stays responsive.
    Agent output (print statements) is captured and shown in the log area.
    A live screenshot preview panel shows what is being sent to the API.
    """

    # Preview panel width in pixels
    _PREVIEW_W = 340

    def __init__(self, root: tk.Tk):
        self.root     = root
        self._thread: threading.Thread | None = None
        self._stop    = threading.Event()
        self._out_q: queue.Queue = queue.Queue()
        self._preview_photo: ImageTk.PhotoImage | None = None
        self._icon_photo: ImageTk.PhotoImage | None = None

        root.title("GenWorker — Desktop AI Agent")
        root.resizable(True, True)
        root.minsize(900, 600)

        self._make_icon()
        self._build_ui()
        self._populate_monitors()
        self._poll_output()

        # Register screenshot hook so the preview updates live
        _computer_mod.screenshot_callback = self._on_screenshot

        # macOS: bring window to front and ensure it receives events
        if IS_MACOS:
            root.lift()
            root.after(100, root.focus_force)
            self._append_log(
                "ℹ️  macOS: If mouse/keyboard control does not work, grant\n"
                "   Accessibility access to Terminal (or your Python app) in\n"
                "   System Settings → Privacy & Security → Accessibility.\n\n"
            )

    # ── App Icon ──────────────────────────────────────────────────────────────

    def _make_icon(self):
        """Generate and apply a simple app icon (green square with white cursor)."""
        try:
            size = 256
            img = Image.new("RGBA", (size, size), (0, 0, 0, 0))
            draw = ImageDraw.Draw(img)

            # Green rounded-square background
            try:
                draw.rounded_rectangle(
                    [8, 8, size - 9, size - 9], radius=48, fill=_GREEN,
                )
            except AttributeError:
                # Pillow < 8.2 fallback
                draw.rectangle([8, 8, size - 9, size - 9], fill=_GREEN)

            # White mouse-cursor arrow
            arrow = [
                (85, 45), (85, 195), (112, 168),
                (148, 218), (168, 198), (132, 148), (175, 128),
            ]
            draw.polygon(arrow, fill="white")

            photo = ImageTk.PhotoImage(img)
            self.root.iconphoto(True, photo)
            self._icon_photo = photo  # prevent garbage collection
        except Exception:
            pass  # icon is cosmetic — don't crash on failure

    # ── UI Construction ────────────────────────────────────────────────────────

    def _build_ui(self):
        pad = {"padx": 8, "pady": 4}

        # ── Dark theme (use 'clam' so colours actually work on macOS) ─────
        self.root.configure(bg=_BG)

        style = ttk.Style()
        style.theme_use("clam")

        style.configure(
            ".", background=_BG, foreground=_FG,
            fieldbackground=_BG_LIGHT, troughcolor=_BG_DARK, borderwidth=0,
        )
        style.configure("TLabel", background=_BG, foreground=_FG)
        style.configure("TFrame", background=_BG)
        style.configure("TCheckbutton", background=_BG, foreground=_FG)
        style.map(
            "TCheckbutton",
            background=[("active", _BG_LIGHT)],
            foreground=[("active", _FG)],
        )
        style.configure(
            "TCombobox", fieldbackground=_BG_LIGHT, foreground=_FG,
            background=_BG_LIGHT, selectbackground=_GREEN,
        )
        style.map("TCombobox", fieldbackground=[("readonly", _BG_LIGHT)])

        # Button styles
        style.configure(
            "Green.TButton", background=_GREEN, foreground="white",
            font=("TkDefaultFont", 10, "bold"), padding=(12, 6),
        )
        style.map(
            "Green.TButton",
            background=[("active", _GREEN_HI), ("disabled", "#1b5e20")],
            foreground=[("disabled", "#666666")],
        )

        style.configure(
            "GreenBig.TButton", background=_GREEN, foreground="white",
            font=("TkDefaultFont", 12, "bold"), padding=(10, 8),
        )
        style.map(
            "GreenBig.TButton",
            background=[("active", _GREEN_HI), ("disabled", "#1b5e20")],
            foreground=[("disabled", "#666666")],
        )

        style.configure(
            "Red.TButton", background=_RED, foreground="white",
            font=("TkDefaultFont", 10, "bold"), padding=(12, 6),
        )
        style.map(
            "Red.TButton",
            background=[("active", _RED_HI), ("disabled", "#4a1a1a")],
            foreground=[("disabled", "#666666")],
        )

        style.configure(
            "Flat.TButton", background=_BG_LIGHT, foreground=_FG,
            padding=(8, 4),
        )
        style.map("Flat.TButton", background=[("active", "#505050")])

        # ── Task input ────────────────────────────────────────────────────
        ttk.Label(
            self.root, text="Task:", font=("TkDefaultFont", 11, "bold"),
        ).pack(anchor="w", **pad)

        task_frame = ttk.Frame(self.root)
        task_frame.pack(fill="x", padx=8, pady=(0, 4))

        self.task_text = tk.Text(
            task_frame,
            font=("TkDefaultFont", 12),
            height=4,
            wrap="word",
            bg=_BG_DARK, fg=_FG, insertbackground="white",
            selectbackground=_GREEN, relief="flat", bd=4,
        )
        self.task_text.pack(side="left", fill="x", expand=True)
        self.task_text.bind("<Shift-Return>", lambda e: "break")
        self.task_text.bind("<Return>", self._on_enter_key)

        send_btn = ttk.Button(
            task_frame, text="▶ Run", command=self._start,
            style="GreenBig.TButton",
        )
        send_btn.pack(side="left", fill="y", padx=(6, 0))

        # ── Options row ──────────────────────────────────────────────────
        opts = ttk.Frame(self.root)
        opts.pack(fill="x", **pad)

        ttk.Label(opts, text="Monitor:").pack(side="left")
        self.monitor_var = tk.IntVar(value=DEFAULT_MONITOR)
        self.monitor_cb  = ttk.Combobox(
            opts, textvariable=self.monitor_var, width=8, state="readonly",
        )
        self.monitor_cb.pack(side="left", padx=(2, 12))

        ttk.Label(opts, text="Timeout (s):").pack(side="left")
        self.timeout_var = tk.IntVar(value=TASK_TIMEOUT)
        tk.Spinbox(
            opts, textvariable=self.timeout_var, from_=30, to=3600, width=6,
            bg=_BG_LIGHT, fg=_FG, buttonbackground=_BG_LIGHT,
            relief="flat", bd=2,
        ).pack(side="left", padx=(2, 12))

        ttk.Label(opts, text="Max steps:").pack(side="left")
        self.steps_var = tk.IntVar(value=MAX_STEPS)
        tk.Spinbox(
            opts, textvariable=self.steps_var, from_=5, to=500, width=6,
            bg=_BG_LIGHT, fg=_FG, buttonbackground=_BG_LIGHT,
            relief="flat", bd=2,
        ).pack(side="left", padx=(2, 12))

        self.thinking_var = tk.BooleanVar(value=True)
        ttk.Checkbutton(
            opts, text="Extended thinking", variable=self.thinking_var,
        ).pack(side="left", padx=(0, 8))

        self.grid_var = tk.BooleanVar(value=SCREENSHOT_GRID_OVERLAY)
        ttk.Checkbutton(
            opts, text="Grid overlay", variable=self.grid_var,
        ).pack(side="left")

        # ── Action buttons ───────────────────────────────────────────────
        btn_row = ttk.Frame(self.root)
        btn_row.pack(fill="x", padx=8, pady=(0, 4))

        self.start_btn = ttk.Button(
            btn_row, text="▶  Start", command=self._start,
            style="Green.TButton",
        )
        self.start_btn.pack(side="left", padx=(0, 8))

        self.stop_btn = ttk.Button(
            btn_row, text="■  Stop", command=self._stop_agent,
            style="Red.TButton", state="disabled",
        )
        self.stop_btn.pack(side="left")

        self.clear_btn = ttk.Button(
            btn_row, text="Clear log", command=self._clear_log,
            style="Flat.TButton",
        )
        self.clear_btn.pack(side="right")

        # ── Status bar ───────────────────────────────────────────────────
        self.status_var = tk.StringVar(value="Ready")
        status_bar = tk.Label(
            self.root, textvariable=self.status_var,
            anchor="w", relief="flat", font=("TkDefaultFont", 9),
            bg=_BG_DARK, fg=_FG_DIM, padx=8, pady=2,
        )
        status_bar.pack(fill="x", side="bottom")

        # ── Content area: log (left) + screenshot preview (right) ────────
        content = ttk.Frame(self.root)
        content.pack(fill="both", expand=True, padx=8, pady=(0, 4))

        # Log panel
        log_frame = ttk.Frame(content)
        log_frame.pack(side="left", fill="both", expand=True)

        ttk.Label(
            log_frame, text="Output:", font=("TkDefaultFont", 11, "bold"),
        ).pack(anchor="w")
        self.log_area = scrolledtext.ScrolledText(
            log_frame,
            state="disabled",
            font=("Courier", 12),
            bg=_BG_DARK, fg="#d4d4d4",
            insertbackground="white",
            wrap="word",
        )
        self.log_area.pack(fill="both", expand=True)

        # Colour tags
        self.log_area.tag_config("error",    foreground="#f44336")
        self.log_area.tag_config("success",  foreground="#4caf50")
        self.log_area.tag_config("info",     foreground="#64b5f6")
        self.log_area.tag_config("thinking", foreground="#ce93d8")
        self.log_area.tag_config("dim",      foreground="#888888")

        # Screenshot preview panel
        preview_frame = tk.Frame(
            content, width=self._PREVIEW_W,
            bg="#111111", relief="flat", bd=1,
        )
        preview_frame.pack(side="right", fill="y", padx=(6, 0))
        preview_frame.pack_propagate(False)

        tk.Label(
            preview_frame, text="Live Preview",
            font=("TkDefaultFont", 10, "bold"),
            bg="#111111", fg="#aaaaaa",
        ).pack(pady=(6, 2))

        self.preview_label = tk.Label(
            preview_frame, bg="#111111",
            text="No screenshot yet",
            fg="#555555", font=("TkDefaultFont", 9),
        )
        self.preview_label.pack(fill="both", expand=True, padx=4, pady=(0, 4))

        self.preview_caption = tk.Label(
            preview_frame, text="", bg="#111111", fg="#777777",
            font=("TkDefaultFont", 8), wraplength=self._PREVIEW_W - 12,
        )
        self.preview_caption.pack(pady=(0, 6))

    def _populate_monitors(self):
        try:
            monitors = list_monitors()
            values   = [f"{i}: {m.name} {m.width}x{m.height}" for i, m in enumerate(monitors)]
        except Exception:
            values = [f"{DEFAULT_MONITOR}: default"]
        self.monitor_cb["values"] = values
        if values:
            self.monitor_cb.current(DEFAULT_MONITOR if DEFAULT_MONITOR < len(values) else 0)

    # ── Keyboard Handling ──────────────────────────────────────────────────────

    def _on_enter_key(self, event):
        """Enter submits; Shift+Enter inserts a newline."""
        # Plain Return → start the agent
        self._start()
        return "break"  # prevent newline being inserted

    # ── Log Helpers ────────────────────────────────────────────────────────────

    def _append_log(self, text: str):
        self.log_area.config(state="normal")
        tag = None
        if any(k in text for k in ("❌", "Error", "error", "⛔", "BLOCKED")):
            tag = "error"
        elif any(k in text for k in ("✅", "🎉", "TASK_COMPLETE", "complete")):
            tag = "success"
        elif any(k in text for k in ("💭", "thinking", "[thinking]")):
            tag = "thinking"
        elif any(k in text for k in ("🧠", "📋", "──", "══", "📊", "ℹ️")):
            tag = "info"

        if tag:
            self.log_area.insert("end", text, tag)
        else:
            self.log_area.insert("end", text)

        self.log_area.see("end")
        self.log_area.config(state="disabled")

    def _clear_log(self):
        self.log_area.config(state="normal")
        self.log_area.delete("1.0", "end")
        self.log_area.config(state="disabled")

    # ── Screenshot Preview ─────────────────────────────────────────────────────

    def _on_screenshot(self, jpeg_bytes: bytes):
        """Called from the agent thread — queue the bytes for main-thread rendering."""
        self._out_q.put(("screenshot", jpeg_bytes))

    def _show_screenshot(self, jpeg_bytes: bytes):
        """Update the preview label — must run on the main thread."""
        try:
            img = Image.open(io.BytesIO(jpeg_bytes))
            # Fit inside the preview panel preserving aspect ratio
            panel_w = self._PREVIEW_W - 8
            panel_h = self.preview_label.winfo_height() or 240
            img.thumbnail((panel_w, panel_h), Image.LANCZOS)
            photo = ImageTk.PhotoImage(img)
            self.preview_label.configure(image=photo, text="")
            self._preview_photo = photo  # keep reference to prevent GC
            self.preview_caption.configure(text=f"{img.width}×{img.height} preview")
        except Exception as exc:
            self.preview_caption.configure(text=f"Preview error: {exc}")

    # ── Agent Control ──────────────────────────────────────────────────────────

    def _start(self):
        task = self.task_text.get("1.0", "end").strip()
        if not task:
            self._append_log("⚠️  Please enter a task first.\n")
            return
        if self._thread and self._thread.is_alive():
            self._append_log("⚠️  An agent is already running.\n")
            return

        try:
            monitor_index = int(self.monitor_cb.get().split(":")[0])
        except (ValueError, IndexError):
            monitor_index = DEFAULT_MONITOR

        enable_thinking = self.thinking_var.get()
        timeout         = self.timeout_var.get()
        grid_overlay    = self.grid_var.get()

        self._stop.clear()
        self.start_btn.config(state="disabled")
        self.stop_btn.config(state="normal")
        self.status_var.set("Running…")
        self._append_log(f"\n{'='*60}\nTask: {task}\n{'='*60}\n")

        self._thread = threading.Thread(
            target=self._run_agent_thread,
            args=(task, monitor_index, enable_thinking, timeout, grid_overlay),
            daemon=True,
        )
        self._thread.start()

    def _stop_agent(self):
        self._stop.set()
        self.status_var.set("Stopping…")
        self._append_log("\n🛑  Stop requested — agent will finish current step.\n")

    def _run_agent_thread(
        self,
        task: str,
        monitor_index: int,
        enable_thinking: bool,
        timeout: int,
        grid_overlay: bool,
    ):
        old_stdout = sys.stdout
        sys.stdout = _QueueWriter(self._out_q)

        try:
            import genworker.config as cfg
            from genworker import agent as agent_mod

            cfg.SCREENSHOT_GRID_OVERLAY = grid_overlay

            agent_mod.run_agent(
                task,
                monitor_index=monitor_index,
                enable_thinking=enable_thinking,
                timeout=timeout,
            )
            self._out_q.put(("log", "\n✅  Agent finished.\n"))
        except Exception as exc:
            self._out_q.put(("log", f"\n❌  Agent error: {exc}\n"))
        finally:
            sys.stdout = old_stdout
            self.root.after(0, self._on_agent_done)

    def _on_agent_done(self):
        self.start_btn.config(state="normal")
        self.stop_btn.config(state="disabled")
        self.status_var.set("Finished")

    # ── Output Polling ─────────────────────────────────────────────────────────

    def _poll_output(self):
        """Drain the output queue on the main thread every 100 ms."""
        try:
            while True:
                kind, payload = self._out_q.get_nowait()
                if kind == "log":
                    self._append_log(payload)
                    line = payload.strip()
                    if line:
                        self.status_var.set(line[:80])
                elif kind == "screenshot":
                    self._show_screenshot(payload)
        except queue.Empty:
            pass
        finally:
            self.root.after(100, self._poll_output)


# ─── Entry Point ───────────────────────────────────────────────────────────────

def launch_gui():
    """Launch the GenWorker GUI window."""
    root = tk.Tk()
    GenWorkerGUI(root)
    root.mainloop()


if __name__ == "__main__":
    launch_gui()
