"""Computer tool: screen capture (with grid overlay), mouse, and keyboard."""

import base64
import ctypes
import ctypes.util
import shutil
import subprocess
import sys
import time
from io import BytesIO

import pyautogui
from PIL import ImageGrab, ImageDraw
from screeninfo import get_monitors

from genworker.config import (
    DISPLAY_WIDTH, DISPLAY_HEIGHT,
    SCREENSHOT_QUALITY, SCREENSHOT_INTERVAL, MOUSE_DURATION, MOD_KEY,
    IS_WINDOWS, IS_MACOS,
    SCREENSHOT_GRID_OVERLAY, GRID_SPACING,
    log,
)

# Optional callback set by the GUI to receive raw JPEG bytes for live preview.
# Signature: callback(jpeg_bytes: bytes) -> None
screenshot_callback = None

# ─── Monitor Helpers ───────────────────────────────────────────────────────────

def list_monitors() -> list:
    monitors = get_monitors()
    print("\n📺  Available monitors:")
    for i, m in enumerate(monitors):
        primary = " ← primary" if getattr(m, "is_primary", False) else ""
        print(f"   [{i}] {m.name}  {m.width}x{m.height}  offset ({m.x},{m.y}){primary}")
    print()
    return monitors


def get_monitor(index: int) -> dict:
    monitors = get_monitors()
    if index < 0 or index >= len(monitors):
        print(f"❌  Monitor {index} not found. Available: 0–{len(monitors)-1}")
        sys.exit(1)
    m = monitors[index]
    return {
        "index":    index,
        "name":     m.name,
        "width":    m.width,
        "height":   m.height,
        "offset_x": m.x,
        "offset_y": m.y,
    }


# ─── macOS Mouse Diagnostics ──────────────────────────────────────────────────

def diagnose_macos_mouse(monitor: dict) -> dict:
    """Run macOS-specific mouse diagnostics and return a (possibly corrected) monitor dict.

    Checks:
      1. Accessibility permission (AXIsProcessTrusted)
      2. Retina / coordinate-scaling mismatch (screeninfo vs pyautogui vs CoreGraphics)
      3. Basic pyautogui functionality (position query)

    Returns the monitor dict — with width/height corrected to logical points if a
    Retina mismatch is detected.
    """
    if not IS_MACOS:
        return monitor

    issues: list[str] = []
    print("\n🔍  macOS mouse diagnostics")
    print("─" * 50)

    # ── 1. Accessibility Permission ───────────────────────────────────────────
    try:
        lib_path = ctypes.util.find_library("ApplicationServices")
        if lib_path:
            appsvcs = ctypes.cdll.LoadLibrary(lib_path)
            appsvcs.AXIsProcessTrusted.restype = ctypes.c_bool
            trusted = appsvcs.AXIsProcessTrusted()
            if trusted:
                print("  ✅  Accessibility: GRANTED")
                log.info("macOS Accessibility permission: granted")
            else:
                msg = (
                    "Accessibility permission DENIED — mouse/keyboard will NOT work.\n"
                    "   → System Settings → Privacy & Security → Accessibility\n"
                    "   → Grant access to Terminal (or your Python app), then restart."
                )
                print(f"  ❌  {msg}")
                log.warning(f"macOS Accessibility: DENIED")
                issues.append(msg)
        else:
            print("  ⚠️  Could not locate ApplicationServices framework")
    except Exception as exc:
        log.warning(f"Accessibility check failed: {exc}")
        print(f"  ⚠️  Accessibility check error: {exc}")

    # ── 2. Retina / Coordinate Scaling ────────────────────────────────────────
    si_w, si_h = monitor["width"], monitor["height"]
    pa_w, pa_h = pyautogui.size()
    print(f"  📐  screeninfo monitor  = {si_w}×{si_h}")
    print(f"  📐  pyautogui.size()    = {pa_w}×{pa_h}")
    log.info(f"screeninfo={si_w}×{si_h}  pyautogui={pa_w}×{pa_h}")

    # CoreGraphics gives us ground-truth logical vs physical dimensions
    cg_logical_w = cg_logical_h = None
    cg_physical_w = cg_physical_h = None
    retina_scale = 1.0

    try:
        from Quartz import (
            CGMainDisplayID,
            CGDisplayPixelsWide,
            CGDisplayPixelsHigh,
            CGDisplayBounds,
            CGDisplayScreenSize,
        )

        display_id = CGMainDisplayID()
        bounds = CGDisplayBounds(display_id)
        cg_logical_w = int(bounds.size.width)
        cg_logical_h = int(bounds.size.height)
        cg_physical_w = CGDisplayPixelsWide(display_id)
        cg_physical_h = CGDisplayPixelsHigh(display_id)

        # On non-Retina displays these are equal; on Retina, physical > logical
        # Note: CGDisplayPixelsWide may return logical on some macOS versions;
        # use CGDisplayModeGetPixelWidth for definitive physical size.
        try:
            from Quartz import CGDisplayCopyDisplayMode, CGDisplayModeGetPixelWidth, CGDisplayModeGetPixelHeight
            mode = CGDisplayCopyDisplayMode(display_id)
            if mode:
                hw_w = CGDisplayModeGetPixelWidth(mode)
                hw_h = CGDisplayModeGetPixelHeight(mode)
                if hw_w > cg_physical_w:
                    cg_physical_w, cg_physical_h = hw_w, hw_h
        except ImportError:
            pass

        if cg_logical_w:
            retina_scale = cg_physical_w / cg_logical_w

        print(f"  📐  CoreGraphics logical  = {cg_logical_w}×{cg_logical_h}")
        print(f"  📐  CoreGraphics physical = {cg_physical_w}×{cg_physical_h}")
        print(f"  📐  Retina scale factor   = {retina_scale:.1f}×")
        log.info(
            f"CoreGraphics logical={cg_logical_w}×{cg_logical_h}  "
            f"physical={cg_physical_w}×{cg_physical_h}  scale={retina_scale:.1f}x"
        )

        # Detect mismatch: screeninfo returning physical pixels instead of logical
        if cg_logical_w and retina_scale > 1.3:
            si_matches_physical = abs(si_w - cg_physical_w) < 50
            si_matches_logical = abs(si_w - cg_logical_w) < 50

            if si_matches_physical and not si_matches_logical:
                msg = (
                    f"screeninfo returned PHYSICAL pixels ({si_w}×{si_h}) "
                    f"instead of logical points ({cg_logical_w}×{cg_logical_h}).\n"
                    f"   pyautogui operates in logical points — clicks would land at "
                    f"{retina_scale:.0f}× the intended position.\n"
                    f"   → Auto-correcting monitor dimensions to logical points."
                )
                print(f"  ⚠️  {msg}")
                log.warning(msg)
                issues.append(msg)

                # Fix the monitor dict in-place
                monitor["width"] = cg_logical_w
                monitor["height"] = cg_logical_h
                print(f"  🔧  Corrected monitor: {cg_logical_w}×{cg_logical_h}")
                log.info(f"Monitor corrected to {cg_logical_w}×{cg_logical_h}")

            elif si_matches_logical:
                print("  ✅  Coordinate scaling: screeninfo matches logical points")
            else:
                print(
                    f"  ⚠️  screeninfo ({si_w}×{si_h}) matches neither logical "
                    f"({cg_logical_w}×{cg_logical_h}) nor physical "
                    f"({cg_physical_w}×{cg_physical_h}) — check monitor index"
                )
        elif cg_logical_w:
            print("  ✅  Non-Retina display — no scaling issues")

    except ImportError:
        log.info("Quartz not importable — skipping CoreGraphics diagnostics")
        print("  ⚠️  Quartz unavailable — cannot verify Retina scaling")

        # Fallback: compare screeninfo with pyautogui
        if pa_w > 0 and si_w > 0:
            ratio = si_w / pa_w
            if 1.8 < ratio < 2.2:
                msg = (
                    f"Likely Retina mismatch: screeninfo ({si_w}×{si_h}) ≈ 2× "
                    f"pyautogui ({pa_w}×{pa_h}).\n"
                    f"   → Auto-correcting monitor to pyautogui dimensions."
                )
                print(f"  ⚠️  {msg}")
                log.warning(msg)
                issues.append(msg)
                monitor["width"] = pa_w
                monitor["height"] = pa_h
                print(f"  🔧  Corrected monitor: {pa_w}×{pa_h}")

    except Exception as exc:
        log.warning(f"CoreGraphics check failed: {exc}")
        print(f"  ⚠️  CoreGraphics check error: {exc}")

    # ── 3. Basic pyautogui Functionality ──────────────────────────────────────
    try:
        pos = pyautogui.position()
        print(f"  ✅  pyautogui.position() = {pos}")
        log.info(f"pyautogui.position() = {pos}")
    except Exception as exc:
        msg = f"pyautogui.position() failed: {exc} — pyobjc may be broken or missing"
        print(f"  ❌  {msg}")
        log.error(msg)
        issues.append(msg)

    # ── Summary ───────────────────────────────────────────────────────────────
    print("─" * 50)
    if issues:
        print(f"  ⚠️  {len(issues)} issue(s) found — see above")
        log.warning(f"macOS diagnostics: {len(issues)} issue(s)")
    else:
        print("  ✅  All checks passed")
        log.info("macOS diagnostics: all checks passed")
    print()

    return monitor


# ─── Grid Overlay ──────────────────────────────────────────────────────────────

def _draw_grid(img, spacing: int = GRID_SPACING):
    """Overlay a coordinate grid on the image.

    Draws grid lines every `spacing` pixels and labels each intersection with
    its (x, y) coordinate in Claude's display space (1280×800). This helps
    Claude identify exact click positions without guessing.
    """
    draw  = ImageDraw.Draw(img)
    w, h  = img.size
    line_color  = (180, 30, 30)   # dark red lines
    label_color = (200, 0, 0)
    label_bg    = (255, 255, 255)

    # Vertical lines
    for x in range(0, w, spacing):
        draw.line([(x, 0), (x, h)], fill=line_color, width=1)

    # Horizontal lines
    for y in range(0, h, spacing):
        draw.line([(0, y), (w, y)], fill=line_color, width=1)

    # Coordinate labels at every other intersection to avoid crowding
    label_step = spacing * 2
    try:
        from PIL import ImageFont
        font = ImageFont.load_default()
    except Exception:
        font = None

    for x in range(0, w, label_step):
        for y in range(0, h, label_step):
            label = f"{x},{y}"
            tx, ty = x + 2, y + 1
            # White background box
            bbox = draw.textbbox((tx, ty), label, font=font)
            draw.rectangle(bbox, fill=label_bg)
            draw.text((tx, ty), label, fill=label_color, font=font)

    return img


# ─── Screen Capture ────────────────────────────────────────────────────────────

def capture_screen(monitor: dict, quality: int = None, grid: bool = None) -> str:
    """Capture the monitor and return a base64-encoded JPEG string.

    When *grid* is True (or the SCREENSHOT_GRID_OVERLAY env var is set), a
    red coordinate grid is drawn on the image before encoding. The grid labels
    give Claude precise (x, y) references in the 1280×800 display space,
    reducing positioning errors on dense UIs.
    """
    if quality is None:
        quality = SCREENSHOT_QUALITY
    if grid is None:
        grid = SCREENSHOT_GRID_OVERLAY

    x0, y0 = monitor["offset_x"], monitor["offset_y"]
    x1, y1 = x0 + monitor["width"], y0 + monitor["height"]
    img = ImageGrab.grab(bbox=(x0, y0, x1, y1), all_screens=True)
    img = img.resize((DISPLAY_WIDTH, DISPLAY_HEIGHT))

    if img.mode == "RGBA":
        img = img.convert("RGB")

    if grid:
        img = _draw_grid(img)

    buf = BytesIO()
    img.save(buf, format="JPEG", quality=quality, optimize=True)
    jpeg_bytes = buf.getvalue()

    if screenshot_callback is not None:
        try:
            screenshot_callback(jpeg_bytes)
        except Exception:
            pass

    return base64.b64encode(jpeg_bytes).decode("utf-8")


def get_screenshot_media_type() -> str:
    return "image/jpeg"


# ─── Coordinate Helpers ────────────────────────────────────────────────────────

def to_screen_coords(x: int, y: int, monitor: dict) -> tuple:
    """Map Claude's 1280×800 coordinate space to real screen pixels."""
    scale_x = monitor["width"]  / DISPLAY_WIDTH
    scale_y = monitor["height"] / DISPLAY_HEIGHT
    sx = int(x * scale_x) + monitor["offset_x"]
    sy = int(y * scale_y) + monitor["offset_y"]
    sx = max(monitor["offset_x"], min(sx, monitor["offset_x"] + monitor["width"]  - 1))
    sy = max(monitor["offset_y"], min(sy, monitor["offset_y"] + monitor["height"] - 1))
    return sx, sy


def validate_coords(x, y) -> bool:
    if not (0 <= x <= DISPLAY_WIDTH and 0 <= y <= DISPLAY_HEIGHT):
        log.warning(f"Coords out of bounds: ({x},{y}) — clamping")
        return False
    return True


# ─── Key Map ───────────────────────────────────────────────────────────────────

_SUPER_KEY = "command" if IS_MACOS else "win"

KEY_MAP = {
    "Return": "enter", "BackSpace": "backspace", "Delete": "delete",
    "Escape": "esc",   "Tab": "tab",             "space": "space",
    "super": _SUPER_KEY, "Super_L": _SUPER_KEY, "Super_R": _SUPER_KEY,
    "ctrl":  "ctrl",  "Control_L": "ctrl",  "Control_R": "ctrl",
    "alt":   "alt",   "Alt_L":     "alt",   "Alt_R":     "alt",
    "shift": "shift", "Shift_L":   "shift", "Shift_R":   "shift",
    "F1":  "f1",  "F2":  "f2",  "F3":  "f3",  "F4":  "f4",
    "F5":  "f5",  "F6":  "f6",  "F7":  "f7",  "F8":  "f8",
    "F9":  "f9",  "F10": "f10", "F11": "f11", "F12": "f12",
    "Page_Up": "pageup", "Page_Down": "pagedown",
    "Home": "home",  "End": "end",
    "Left": "left",  "Right": "right", "Up": "up", "Down": "down",
    "win": _SUPER_KEY,
    "command": "command", "Command_L": "command", "Command_R": "command",
    "option":  "option",  "Option_L":  "option",  "Option_R":  "option",
}


# ─── Computer Action Executor ──────────────────────────────────────────────────

def execute_computer_action(inp: dict, monitor: dict) -> str:
    action = inp.get("action", "unknown")
    log.info(f"⚡  computer/{action} | {inp}")

    def sc(coord):
        x, y = int(coord[0]), int(coord[1])
        validate_coords(x, y)
        return to_screen_coords(x, y, monitor)

    try:
        if action == "screenshot":
            return "__screenshot__"

        elif action == "left_click":
            rx, ry = sc(inp["coordinate"])
            pyautogui.click(rx, ry, duration=MOUSE_DURATION)
            return f"Left click ({rx},{ry})"

        elif action == "right_click":
            rx, ry = sc(inp["coordinate"])
            pyautogui.rightClick(rx, ry, duration=MOUSE_DURATION)
            return f"Right click ({rx},{ry})"

        elif action == "middle_click":
            rx, ry = sc(inp["coordinate"])
            pyautogui.middleClick(rx, ry, duration=MOUSE_DURATION)
            return f"Middle click ({rx},{ry})"

        elif action == "double_click":
            rx, ry = sc(inp["coordinate"])
            pyautogui.doubleClick(rx, ry, duration=MOUSE_DURATION)
            return f"Double click ({rx},{ry})"

        elif action == "triple_click":
            rx, ry = sc(inp["coordinate"])
            pyautogui.click(rx, ry, clicks=3, interval=0.05)
            return f"Triple click ({rx},{ry})"

        elif action == "mouse_move":
            rx, ry = sc(inp["coordinate"])
            pyautogui.moveTo(rx, ry, duration=MOUSE_DURATION)
            return f"Mouse move ({rx},{ry})"

        elif action == "left_click_drag":
            sx, sy = sc(inp["start_coordinate"])
            ex, ey = sc(inp["end_coordinate"])
            pyautogui.moveTo(sx, sy, duration=0.15)
            pyautogui.mouseDown()
            time.sleep(0.05)
            pyautogui.moveTo(ex, ey, duration=MOUSE_DURATION)
            pyautogui.mouseUp()
            return f"Drag ({sx},{sy}) → ({ex},{ey})"

        elif action == "left_mouse_down":
            pyautogui.mouseDown()
            return "Mouse down"

        elif action == "left_mouse_up":
            pyautogui.mouseUp()
            return "Mouse up"

        elif action == "cursor_position":
            return f"Cursor at {pyautogui.position()}"

        elif action == "key":
            key_str = inp["text"]
            parts   = key_str.split("+")
            mapped  = [KEY_MAP.get(p.strip(), p.strip().lower()) for p in parts]
            if len(mapped) > 1:
                pyautogui.hotkey(*mapped)
            else:
                pyautogui.press(mapped[0])
            return f"Key: {key_str}"

        elif action == "hold_key":
            key_str  = inp["text"]
            mapped   = KEY_MAP.get(key_str, key_str.lower())
            duration = inp.get("duration", 1)
            pyautogui.keyDown(mapped)
            time.sleep(duration)
            pyautogui.keyUp(mapped)
            return f"Hold key: {key_str} ({duration}s)"

        elif action == "type":
            text  = inp["text"]
            typed = False
            # Method 1: Clipboard paste (fast, supports unicode)
            try:
                import pyperclip
                pyperclip.copy(text)
                time.sleep(0.05)
                pyautogui.hotkey(MOD_KEY, "v")
                time.sleep(0.15)
                typed = True
            except Exception:
                pass
            # Method 2: OS-native clipboard fallback
            if not typed:
                try:
                    if IS_WINDOWS:
                        subprocess.run(
                            ["powershell", "-NoProfile", "-Command",
                             f"Set-Clipboard -Value '{text.replace(chr(39), chr(39)+chr(39))}'"],
                            timeout=5, capture_output=True,
                        )
                    elif IS_MACOS:
                        subprocess.run(
                            ["pbcopy"], input=text, text=True, timeout=5, capture_output=True,
                        )
                    else:
                        if shutil.which("xclip"):
                            subprocess.run(
                                ["xclip", "-selection", "clipboard"],
                                input=text, text=True, timeout=5, capture_output=True,
                            )
                        elif shutil.which("xsel"):
                            subprocess.run(
                                ["xsel", "--clipboard", "--input"],
                                input=text, text=True, timeout=5, capture_output=True,
                            )
                    pyautogui.hotkey(MOD_KEY, "v")
                    time.sleep(0.15)
                    typed = True
                except Exception:
                    pass
            # Method 3: Direct typewrite (ASCII only, slow)
            if not typed:
                pyautogui.typewrite(text, interval=0.03)
            return f"Typed: {text[:100]}"

        elif action == "scroll":
            rx, ry    = sc(inp["coordinate"])
            direction = inp.get("direction", "down")
            amount    = int(inp.get("amount", 3))
            delta     = -amount if direction == "down" else amount
            pyautogui.scroll(delta, x=rx, y=ry)
            return f"Scroll {direction} {amount}x at ({rx},{ry})"

        elif action == "wait":
            duration = inp.get("duration", 1)
            time.sleep(duration)
            return f"Waited {duration}s"

        else:
            return f"Unknown action: {action}"

    except KeyError as e:
        return f"Missing parameter: {e}"
    except Exception as e:
        log.error(f"Error executing {action}: {e}")
        return f"Error: {e}"


# ─── Adaptive Screenshot Delay ─────────────────────────────────────────────────

def get_adaptive_delay(action: str, inp: dict) -> float:
    """Return an appropriate wait time after an action before capturing a screenshot.

    GUI-loading actions (opening apps, pressing Enter) need longer waits than
    simple typing or scrolling.
    """
    if action == "key":
        text = inp.get("text", "")
        if "Return" in text or "enter" in text.lower():
            return 1.5
        if "super" in text.lower() or "win" in text.lower():
            return 1.5
        return 0.5
    if action == "type":
        return 0.3
    if action == "scroll":
        return 0.5
    slow = {"left_click": 1.2, "double_click": 1.5}
    return slow.get(action, SCREENSHOT_INTERVAL)
