#!/usr/bin/env python3
"""Iris.app entry point.

Runs the agent loop (imported from Resources/agent.py) and hosts a tiny
menubar indicator. Designed to run under the macOS system python3 with no
third-party dependencies:

  - Menubar status item via PyObjC (ships with macOS system python3).
    If PyObjC is unavailable the app still runs headless.
  - First-run setup via osascript dialogs (zero dependencies).
  - "Start at login" via ServiceManagement SMAppService when available,
    with documented manual fallback.

Stdlib + PyObjC only.
"""

from __future__ import annotations

import os
import subprocess
import sys
import threading
import time

HERE = os.path.dirname(os.path.abspath(__file__))          # Contents/MacOS
RESOURCES = os.path.normpath(os.path.join(HERE, "..", "Resources"))
sys.path.insert(0, RESOURCES)

import agent as agent_mod  # noqa: E402  (Resources/agent.py)

APP_NAME = agent_mod.APP_NAME  # single source of truth for the name
BUNDLE_ID = "com.staticspark.iris"

# Module-level stop event shared by the menubar Quit action and the agent
# thread (AppDelegate methods can't see run_with_menubar's locals).
_STOP = threading.Event()


# ---------------------------------------------------------------------------
# osascript dialogs (first-run setup)
# ---------------------------------------------------------------------------

def _osascript(script: str) -> str:
    r = subprocess.run(["osascript", "-e", script],
                       capture_output=True, text=True, timeout=120)
    return (r.stdout or "").strip()


def ask_dialog(text: str, buttons=("OK",), default=None) -> str | None:
    btn_list = '{"' + '", "'.join(buttons) + '"}'
    script = (
        'tell application "System Events"\n'
        '  activate\n'
        f'  set _r to button returned of (display dialog "{text}" '
        f'with title "{APP_NAME}" buttons {btn_list}'
        + (f' default button "{default}"' if default else "")
        + ')\n'
        '  return _r\n'
        'end tell'
    )
    try:
        out = _osascript(script)
        return out or None
    except Exception:  # noqa: BLE001
        return None


def notify_user(title: str, text: str) -> None:
    try:
        subprocess.run(["osascript", "-e",
                        f'display notification "{text}" with title "{title}"'],
                       capture_output=True, timeout=10)
    except Exception:  # noqa: BLE001
        pass


def open_system_settings(pane: str) -> None:
    # pane e.g. "Privacy_ScreenCapture" or "Privacy_Accessibility"
    url = f"x-apple.systempreferences:com.apple.preference.security?{pane}"
    subprocess.run(["open", url], capture_output=True)


# ---------------------------------------------------------------------------
# Permissions
# ---------------------------------------------------------------------------

def peekaboo_bin() -> str | None:
    return agent_mod.find_peekaboo()


def permission_check() -> dict:
    """Heuristic check of Screen Recording + Accessibility grants.

    Returns {"screen_recording": True|False|None, "accessibility": ...}.
    None = could not determine. The test screenshot is the ground truth.
    """
    binary = peekaboo_bin()
    if not binary:
        return {"screen_recording": None, "accessibility": None,
                "error": "no peekaboo"}
    try:
        r = subprocess.run([binary, "permissions", "status"],
                           capture_output=True, text=True, timeout=30)
        raw = ((r.stdout or "") + "\n" + (r.stderr or "")).lower()
    except Exception as e:  # noqa: BLE001
        return {"screen_recording": None, "accessibility": None,
                "error": str(e)[:200]}

    def granted(keywords: list[str]) -> bool | None:
        idxs = [raw.find(k) for k in keywords if raw.find(k) >= 0]
        if not idxs:
            return None
        window = raw[min(idxs):min(idxs) + 400]
        positive = any(w in window for w in
                       ("granted", "authorized", "allowed", "enabled", "true"))
        negative = any(w in window for w in
                       ("not granted", "denied", "not authorized", "missing",
                        "disabled", "false", "not enabled"))
        if positive and not negative:
            return True
        if negative and not positive:
            return False
        return None

    return {
        "screen_recording": granted(["screen recording", "screen & system audio"]),
        "accessibility": granted(["accessibility"]),
    }


def take_test_screenshot() -> tuple[bool, str]:
    out = os.path.join(agent_mod.app_home(), "setup-test.png")
    code, err = agent_mod.run_peekaboo(
        ["image", "--mode", "screen", "--path", out])
    if code == 0 and os.path.exists(out):
        return True, out
    return False, err[:300]


def first_run_setup() -> None:
    """Interactive onboarding. Runs once (marker file in app home)."""
    home = agent_mod.app_home()
    marker = os.path.join(home, ".onboarded")
    if os.path.exists(marker):
        return
    agent_mod.ensure_dirs()

    choice = ask_dialog(
        f"Welcome to {APP_NAME} — it gives your AI assistant eyes on this "
        "Mac (built for driving Roblox Studio).\\n\\n"
        "You'll grant two macOS permissions, then take a test screenshot.",
        buttons=("Quit", "Continue"), default="Continue")
    if choice != "Continue":
        sys.exit(0)

    if not peekaboo_bin():
        ask_dialog(f"The screen-capture helper wasn't found inside the app.\\n\\n"
                   f"Reinstall {APP_NAME} and try again.",
                   buttons=("Quit",), default="Quit")
        sys.exit(1)

    # --- Screen Recording ---
    if permission_check().get("screen_recording") is not True:
        choice = ask_dialog(
            "Step 1 of 2: Screen Recording.\\n\\n"
            "macOS will now ask for permission to record the screen. "
            "Click Continue, then Allow when the system prompt appears.",
            buttons=("Back", "Continue"), default="Continue")
        if choice != "Continue":
            sys.exit(0)
        binary = peekaboo_bin()
        # Trigger the one system prompt macOS allows, then attempt a
        # capture — the first capture is what actually prompts.
        subprocess.run([binary, "permissions", "request", "screen-recording"],
                       capture_output=True, timeout=30)
        subprocess.run([binary, "image", "--mode", "screen",
                        "--path", os.path.join(home, "onboarding-probe.png")],
                       capture_output=True, timeout=30)
        open_system_settings("Privacy_ScreenCapture")
        ask_dialog("If you saw the system prompt, allow it.\\n\\n"
                   "Otherwise, in System Settings → Privacy & Security → "
                   "Screen & System Audio Recording, turn on the Iris "
                   "capture helper, then press Continue.",
                   buttons=("Continue",), default="Continue")

    # --- Accessibility (needed for click/type) ---
    if permission_check().get("accessibility") is not True:
        open_system_settings("Privacy_Accessibility")
        ask_dialog("Step 2 of 2: Accessibility (for clicks and typing).\\n\\n"
                   "In System Settings → Privacy & Security → Accessibility, "
                   "turn on the Iris capture helper, then press Continue.",
                   buttons=("Skip", "Continue"), default="Continue")

    # --- Test screenshot (ground truth) ---
    ok, info = take_test_screenshot()
    if ok:
        choice = ask_dialog("Test screenshot captured.\\n\\n"
                            "Open it in Preview to confirm it shows your "
                            "screen (not black).",
                            buttons=("Show me", "Done"), default="Done")
        if choice == "Show me":
            subprocess.run(["open", info], capture_output=True)
    else:
        ask_dialog("Test screenshot FAILED.\\n\\n"
                   f"{info}\\n\\n"
                   "Most likely Screen Recording isn't on yet — grant it in "
                   "System Settings, then use the menubar → Re-run setup.",
                   buttons=("OK",), default="OK")

    # --- Start at login ---
    choice = ask_dialog(f"Start {APP_NAME} automatically when you log in?",
                        buttons=("Not now", "Yes"), default="Yes")
    if choice == "Yes":
        ok, msg = set_login_item(True)
        if not ok:
            ask_dialog("Automatic start couldn't be enabled from here.\\n\\n"
                       + msg, buttons=("OK",), default="OK")

    with open(marker, "w") as f:
        f.write(agent_mod.VERSION + "\n")


def rerun_setup() -> None:
    """Menubar action: wipe the marker and run setup again."""
    try:
        os.remove(os.path.join(agent_mod.app_home(), ".onboarded"))
    except OSError:
        pass
    first_run_setup()


# ---------------------------------------------------------------------------
# Login item
# ---------------------------------------------------------------------------

def set_login_item(enable: bool) -> tuple[bool, str]:
    """Try SMAppService; fall back to manual instructions."""
    try:
        from ServiceManagement import SMAppService  # type: ignore
        svc = SMAppService.mainApp()
        if enable:
            svc.register()
        else:
            svc.unregister()
        return True, ""
    except Exception as e:  # noqa: BLE001
        manual = ("Manual steps: System Settings → General → Login Items → "
                  f"press + and add {APP_NAME}.app from Applications.")
        if enable:
            return False, f"({e})\n\n{manual}".strip()
        return False, str(e)[:200]


# ---------------------------------------------------------------------------
# Menubar (PyObjC) with headless fallback
# ---------------------------------------------------------------------------

def run_with_menubar() -> int:
    """Returns 0 if the menubar ran, 1 if PyObjC unavailable (caller falls back)."""
    try:
        import objc  # noqa: F401
        from Cocoa import (NSApplication, NSStatusBar, NSMenu, NSMenuItem,
                           NSVariableStatusItemLength,
                           NSApplicationActivationPolicyAccessory)
        from Foundation import NSObject
    except Exception:
        return 1

    class AppDelegate(NSObject):
        def init(self):
            self = objc.super(AppDelegate, self).init()
            if self is None:
                return None
            self.status_item = None
            return self

        def applicationDidFinishLaunching_(self, notification):  # noqa: N802
            app = NSApplication.sharedApplication()
            app.setActivationPolicy_(NSApplicationActivationPolicyAccessory)
            bar = NSStatusBar.systemStatusBar()
            self.status_item = bar.statusItemWithLength_(NSVariableStatusItemLength)
            self.status_item.setTitle_("◉")
            self.status_item.setToolTip_(f"{APP_NAME} running")
            menu = NSMenu.alloc().init()

            def add(title, action, enabled=True):
                item = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_(
                    title, action, "")
                item.setEnabled_(enabled)
                menu.addItem_(item)
                return item

            add(f"{APP_NAME} running (v{agent_mod.VERSION})", None, enabled=False)
            menu.addItem_(NSMenuItem.separatorItem())
            add("Take test screenshot", "takeTestScreenshot:")
            add("Re-run setup…", "rerunSetup:")
            menu.addItem_(NSMenuItem.separatorItem())
            add(f"Quit {APP_NAME}", "quitApp:")
            self.status_item.setMenu_(menu)

        # -- actions --------------------------------------------------
        def takeTestScreenshot_(self, sender):  # noqa: N802
            ok, info = take_test_screenshot()
            if ok:
                subprocess.run(["open", info], capture_output=True)
            else:
                notify_user(APP_NAME, f"Screenshot failed: {info[:120]}")

        def rerunSetup_(self, sender):  # noqa: N802
            rerun_setup()

        def quitApp_(self, sender):  # noqa: N802
            _STOP.set()
            NSApplication.sharedApplication().terminate_(self)

    _STOP.clear()
    t = threading.Thread(target=agent_mod.run,
                         kwargs={"stop_event": _STOP},
                         daemon=True)
    t.start()

    app = NSApplication.sharedApplication()
    delegate = AppDelegate.alloc().init()
    app.setDelegate_(delegate)
    app.run()
    _STOP.set()
    t.join(timeout=10)
    return 0


def run_headless() -> int:
    print("PyObjC unavailable — running headless (no menubar).", flush=True)
    return agent_mod.run()


def main() -> int:
    try:
        first_run_setup()
    except SystemExit:
        raise
    except Exception as e:  # noqa: BLE001
        print(f"setup error (continuing): {e}", flush=True)
    if run_with_menubar() == 0:
        return 0
    return run_headless()


if __name__ == "__main__":
    raise SystemExit(main())
