"""
First-run setup wizard for the Terminull menu-bar app (macOS).

Replaces the old `rumps.alert(...)` welcome dialog, which was a plain NSAlert the user
could dismiss with a single stray click/Return — accidentally skipping the whole intro.
Instead this is a small, self-contained multi-page window (PyObjC / AppKit):

  • no minimize/zoom traffic-light buttons — only the close (✕) button, which cancels the
    whole wizard (the setup is informational + shows the pairing QR, so cancelling is safe);
  • bottom bar advances Next → Finish (no Back — the flow is short and stateless).

Pages:
  1. Welcome — explains that AI plugins and higher-quality voice (STT) models download and
     load in the background when enabled, so those features may not be available immediately.
  2. Pair — renders the pairing QR inline for the iOS app to scan.

Runs on the main thread (AppKit requirement) — `_show_first_run_pairing` already drains the
status queue on the main thread, so calling `present(...)` from there is safe. The controller
is retained by the caller (the menu-bar app) so the non-modal window isn't garbage-collected.

Falls back to the caller's plain-alert path if AppKit window creation ever fails (headless /
odd environments), so first-run pairing never hard-depends on this window.
"""
from __future__ import annotations

import logging
import os
import tempfile
import threading

from terminull_bridge import config as cfg

log = logging.getLogger(__name__)

# Feature toggles surfaced on the welcome page. Each loads a model / spins up work in the
# background when enabled, so they can't all be "on and ready" the instant setup finishes —
# which is exactly what the welcome copy warns about. Kept in sync with the menu-bar toggles.
_TOGGLES = [
    ("stt_enabled", True, "Voice input (speech-to-text)",
     "Dictate to your agents. Downloads a local voice model on first use."),
    ("summarizer_enabled", True, "AI assists",
     "On-device smart replies, summaries, and session titles."),
]

# Window geometry (fixed size — the content is short and known).
_W = 480
_H = 440
_MARGIN = 28
_QR_SIZE = 220

# ObjC helper classes (window delegate + button/target). These MUST be registered with the
# ObjC runtime exactly ONCE — PyObjC keys classes by name globally, so defining them inside a
# method re-registers on every wizard presentation → "_Target is overriding existing
# Objective-C class" on the 2nd show (e.g. first-run wizard, then the manual "Add device by
# QR"). So build each class lazily on first use and cache it here; every wizard instance
# reuses the same class and dispatches via a per-instance `wizard` back-reference.
_TARGET_CLS = None
_DELEGATE_CLS = None


def _target_class():
    global _TARGET_CLS
    if _TARGET_CLS is None:
        from AppKit import NSObject

        class _WizardTarget(NSObject):
            def advance_(self, _sender):   # noqa: N802 — objc selector
                if self.wizard is not None:
                    self.wizard._advance()

            def toggle_(self, sender):     # noqa: N802 — objc selector
                if self.wizard is not None:
                    self.wizard._on_toggle(sender)

        _TARGET_CLS = _WizardTarget
    return _TARGET_CLS


def _delegate_class():
    global _DELEGATE_CLS
    if _DELEGATE_CLS is None:
        from AppKit import NSObject

        class _WizardWindowDelegate(NSObject):
            def windowWillClose_(self, _note):  # noqa: N802 — objc selector
                if self.wizard is not None:
                    self.wizard._cleanup()

        _DELEGATE_CLS = _WizardWindowDelegate
    return _DELEGATE_CLS


class SetupWizard:
    """A retained controller wrapping the AppKit window. One instance per presentation."""

    def __init__(self, pair_url: str | None, pairing_only: bool = False,
                 on_finish=None) -> None:
        self._pair_url = pair_url
        self._pairing_only = pairing_only  # single "Pair your iPhone" page (manual "Add device")
        self._on_finish = on_finish        # called after the window closes (menu resync)
        self._page = 0
        self._window = None
        self._delegate = None
        self._page_views: list = []
        self._page_kinds: list = []       # parallel to _page_views: "welcome"|"folders"|"pairing"
        self._next_button = None
        self._page_label = None
        self._qr_tmp: str | None = None
        self._toggle_buttons: list = []  # (feature_key, NSButton)
        self._folder_access_armed = False  # fire the OS folder prompts at most once

    # ── public ───────────────────────────────────────────────────────────────

    def present(self) -> bool:
        """Build + show the window. Returns False if AppKit is unavailable so the caller
        can fall back to the old alert flow."""
        try:
            from AppKit import (
                NSApp,
                NSBackingStoreBuffered,
                NSColor,
                NSObject,
                NSWindow,
                NSWindowStyleMaskClosable,
                NSWindowStyleMaskTitled,
                NSWindowMiniaturizeButton,
                NSWindowZoomButton,
            )
            from Foundation import NSMakeRect
        except Exception as e:  # pragma: no cover - only on non-AppKit hosts
            log.info("setup wizard: AppKit unavailable (%s) — falling back to alert", e)
            return False

        try:
            rect = NSMakeRect(0, 0, _W, _H)
            style = NSWindowStyleMaskTitled | NSWindowStyleMaskClosable
            win = NSWindow.alloc().initWithContentRect_styleMask_backing_defer_(
                rect, style, NSBackingStoreBuffered, False
            )
            win.setTitle_("Pair Device" if self._pairing_only else "Terminull Setup")
            win.setReleasedWhenClosed_(False)  # we hold the ref; avoid double-free
            # Only the close (✕) button — no minimize/zoom, so the wizard can't be shaded away.
            for btn in (NSWindowMiniaturizeButton, NSWindowZoomButton):
                b = win.standardWindowButton_(btn)
                if b is not None:
                    b.setHidden_(True)
            try:
                win.setBackgroundColor_(NSColor.windowBackgroundColor())
            except Exception:
                pass

            content = win.contentView()
            self._build_pages(content)
            self._build_footer(content)
            self._window = win

            # Close (✕) cancels the whole wizard — capture it via a delegate for cleanup.
            self._delegate = _delegate_class().alloc().init()
            self._delegate.wizard = self
            win.setDelegate_(self._delegate)

            self._show_page(0)
            win.center()
            # Accessory (menu-bar) apps still need an explicit activate to front the window.
            try:
                NSApp.activateIgnoringOtherApps_(True)
            except Exception:
                pass
            win.makeKeyAndOrderFront_(None)
            return True
        except Exception as e:  # pragma: no cover - defensive
            log.warning("setup wizard: build failed (%s) — falling back to alert", e)
            self._cleanup()
            return False

    # ── page construction ────────────────────────────────────────────────────

    def _build_pages(self, content) -> None:
        from AppKit import NSView
        from Foundation import NSMakeRect

        # Page area sits above the ~64pt footer.
        page_h = _H - 64
        page_rect = NSMakeRect(0, 64, _W, page_h)

        if not self._pairing_only:
            page1 = NSView.alloc().initWithFrame_(page_rect)
            self._fill_welcome(page1, page_h)
            content.addSubview_(page1)
            self._page_views.append(page1)
            self._page_kinds.append("welcome")

            folder = NSView.alloc().initWithFrame_(page_rect)
            self._fill_folders(folder, page_h)
            content.addSubview_(folder)
            self._page_views.append(folder)
            self._page_kinds.append("folders")

        page2 = NSView.alloc().initWithFrame_(page_rect)
        self._fill_pairing(page2, page_h)
        content.addSubview_(page2)
        self._page_views.append(page2)
        self._page_kinds.append("pairing")

    def _fill_welcome(self, view, page_h: float) -> None:
        self._add_title(view, "Welcome to Terminull", page_h)
        body = (
            "Terminull runs in your menu bar and connects your iPhone to the coding agents "
            "on this Mac. Enable the extras below — each downloads and loads its model in the "
            "background, so it lights up a little after you switch it on, not instantly. You "
            "can change these anytime from the menu-bar icon."
        )
        self._add_body(view, body, page_h, top=72, height=76)

        # Feature toggles — each kicks off a background model load when enabled.
        row_h = 52
        top = 72 + 76 + 12
        for i, (key, default, label, sub) in enumerate(_TOGGLES):
            self._add_toggle(view, page_h, key, default, label, sub, top + i * row_h, row_h)

    def _fill_folders(self, view, page_h: float) -> None:
        self._add_title(view, "Files & Folders", page_h)
        body = (
            "Terminull lets your agents read and attach files — screenshots, notes, code — "
            "from this Mac. macOS gates your Desktop, Documents, and Downloads folders, so it "
            "will ask for access to each in a moment.\n\n"
            "Click Allow on the prompts to browse and attach files from those folders. You can "
            "skip any of them and grant access later in System Settings → Privacy & Security. "
            "Terminull never uploads files on its own — only what you attach in a message."
        )
        self._add_body(view, body, page_h, top=72, height=page_h - 96)

    def _maybe_trigger_folder_access(self) -> None:
        """~2s after the Files & Folders page appears, touch the three TCC-gated folders so
        macOS surfaces their access prompts (the user has just read why). Marker-gated +
        armed-once so it never double-fires; a denied folder just stays inaccessible."""
        if self._folder_access_armed:
            return
        self._folder_access_armed = True

        from pathlib import Path
        marker = Path.home() / ".terminull" / ".folder_access_prewarmed"
        try:
            if marker.exists():
                return
        except Exception:
            return

        def _touch() -> None:
            home = Path.home()
            for sub in ("Desktop", "Documents", "Downloads"):
                p = home / sub
                try:
                    if p.is_dir():
                        os.listdir(p)   # trips TCC → the macOS access prompt
                except Exception as e:
                    log.info("wizard folder access: %s not accessible (%s)", sub, e)
            try:
                marker.parent.mkdir(parents=True, exist_ok=True)
                marker.write_text("")   # written even on denial — macOS won't re-prompt anyway
            except Exception:
                pass

        threading.Timer(2.0, _touch).start()

    def _fill_pairing(self, view, page_h: float) -> None:
        from AppKit import NSImage, NSImageView
        from Foundation import NSMakeRect

        self._add_title(view, "Pair your iPhone", page_h)
        self._add_body(
            view,
            "Open the Terminull app on your iPhone and scan this code to connect.",
            page_h,
            top=76,
            height=52,
        )

        # QR image, centered.
        qr_y = page_h - 76 - 52 - _QR_SIZE - 8
        img_rect = NSMakeRect((_W - _QR_SIZE) / 2, max(qr_y, 24), _QR_SIZE, _QR_SIZE)
        img_view = NSImageView.alloc().initWithFrame_(img_rect)
        ns_img = self._render_qr()
        if ns_img is not None:
            img_view.setImage_(ns_img)
        else:
            # No QR yet (relay not connected) — swap in an explanatory line.
            self._add_body(
                view,
                "The bridge hasn't connected to the relay yet. Reopen “Add device by QR” "
                "from the menu bar once the icon shows it's connected.",
                page_h,
                top=140,
                height=80,
            )
        view.addSubview_(img_view)

    def _render_qr(self):
        """Render the pairing URL to an NSImage, or None if unavailable."""
        if not self._pair_url:
            return None
        try:
            import qrcode
            from AppKit import NSImage

            img = qrcode.make(self._pair_url)
            tmp = tempfile.NamedTemporaryFile(
                suffix=".png", delete=False, prefix="terminull_wizard_qr_"
            )
            tmp.close()
            img.save(tmp.name)
            self._qr_tmp = tmp.name
            ns_img = NSImage.alloc().initWithContentsOfFile_(tmp.name)
            return ns_img
        except Exception as e:
            log.info("setup wizard: QR render failed (%s)", e)
            return None

    def _build_footer(self, content) -> None:
        from AppKit import NSButton, NSTextField
        from Foundation import NSMakeRect

        # Page indicator, bottom-left.
        lbl = NSTextField.alloc().initWithFrame_(NSMakeRect(_MARGIN, 22, 120, 20))
        lbl.setBezeled_(False)
        lbl.setDrawsBackground_(False)
        lbl.setEditable_(False)
        lbl.setSelectable_(False)
        try:
            from AppKit import NSColor, NSFont
            lbl.setTextColor_(NSColor.secondaryLabelColor())
            lbl.setFont_(NSFont.systemFontOfSize_(11))
        except Exception:
            pass
        content.addSubview_(lbl)
        self._page_label = lbl

        # Next / Finish, bottom-right.
        btn_w = 96
        btn = NSButton.alloc().initWithFrame_(
            NSMakeRect(_W - _MARGIN - btn_w, 16, btn_w, 32)
        )
        btn.setTitle_("Next")
        btn.setBezelStyle_(1)  # NSBezelStyleRounded
        btn.setTarget_(self._target_obj())
        btn.setAction_("advance:")
        try:
            btn.setKeyEquivalent_("\r")  # Return activates the default button
        except Exception:
            pass
        content.addSubview_(btn)
        self._next_button = btn

    def _target_obj(self):
        # A retained instance of the module-level target class (registered once), holding a
        # back-reference to this wizard. Reused for the footer button + every toggle.
        if getattr(self, "_target", None) is not None:
            return self._target
        self._target = _target_class().alloc().init()
        self._target.wizard = self
        return self._target

    def _on_toggle(self, sender) -> None:
        for key, btn in self._toggle_buttons:
            if btn is sender:
                try:
                    cfg.set_feature(key, bool(sender.state()))
                except Exception as e:
                    log.warning("setup wizard: set_feature(%s) failed: %s", key, e)
                return

    # ── helpers ──────────────────────────────────────────────────────────────

    def _add_title(self, view, text: str, page_h: float) -> None:
        from AppKit import NSFont, NSTextField
        from Foundation import NSMakeRect

        f = NSTextField.alloc().initWithFrame_(
            NSMakeRect(_MARGIN, page_h - 52, _W - 2 * _MARGIN, 30)
        )
        f.setStringValue_(text)
        f.setBezeled_(False)
        f.setDrawsBackground_(False)
        f.setEditable_(False)
        f.setSelectable_(False)
        f.setFont_(NSFont.boldSystemFontOfSize_(20))
        view.addSubview_(f)

    def _add_body(self, view, text: str, page_h: float, top: float, height: float) -> None:
        from AppKit import NSFont, NSTextField
        from Foundation import NSMakeRect

        f = NSTextField.alloc().initWithFrame_(
            NSMakeRect(_MARGIN, page_h - top - height, _W - 2 * _MARGIN, height)
        )
        f.setStringValue_(text)
        f.setBezeled_(False)
        f.setDrawsBackground_(False)
        f.setEditable_(False)
        f.setSelectable_(True)
        f.setFont_(NSFont.systemFontOfSize_(13))
        try:
            cell = f.cell()
            cell.setWraps_(True)
            cell.setLineBreakMode_(0)  # NSLineBreakByWordWrapping
        except Exception:
            pass
        view.addSubview_(f)

    def _add_toggle(self, view, page_h: float, key: str, default: bool,
                    label: str, sub: str, top: float, row_h: float) -> None:
        from AppKit import NSButton, NSColor, NSFont, NSTextField
        from Foundation import NSMakeRect

        try:
            on = bool(cfg.get_feature(key, default=default))
        except Exception:
            on = default

        # Switch with the primary label as its title.
        sw = NSButton.alloc().initWithFrame_(
            NSMakeRect(_MARGIN, page_h - top - 22, _W - 2 * _MARGIN, 22)
        )
        sw.setButtonType_(3)   # NSButtonTypeSwitch (checkbox/switch)
        sw.setTitle_(label)
        sw.setFont_(NSFont.systemFontOfSize_(13))
        sw.setState_(1 if on else 0)
        sw.setTarget_(self._target_obj())
        sw.setAction_("toggle:")
        view.addSubview_(sw)
        self._toggle_buttons.append((key, sw))

        # Secondary description under the switch.
        desc = NSTextField.alloc().initWithFrame_(
            NSMakeRect(_MARGIN + 20, page_h - top - row_h + 6, _W - 2 * _MARGIN - 20, 18)
        )
        desc.setStringValue_(sub)
        desc.setBezeled_(False)
        desc.setDrawsBackground_(False)
        desc.setEditable_(False)
        desc.setSelectable_(False)
        desc.setFont_(NSFont.systemFontOfSize_(11))
        try:
            desc.setTextColor_(NSColor.secondaryLabelColor())
        except Exception:
            pass
        view.addSubview_(desc)

    def _show_page(self, idx: int) -> None:
        self._page = idx
        last = len(self._page_views) - 1
        for i, v in enumerate(self._page_views):
            v.setHidden_(i != idx)
        n = len(self._page_views)
        if self._next_button is not None:
            if idx >= last:
                self._next_button.setTitle_("Done" if n == 1 else "Finish")
            else:
                self._next_button.setTitle_("Next")
        if self._page_label is not None:
            # No "1 of 1" for the single-page (manual pairing) mode.
            self._page_label.setStringValue_("" if n == 1 else f"{idx + 1} of {n}")
        # When the Files & Folders page appears, fire the macOS access prompts ~2s later
        # (the user has just read why they're needed).
        if idx < len(self._page_kinds) and self._page_kinds[idx] == "folders":
            self._maybe_trigger_folder_access()

    def _advance(self) -> None:
        last = len(self._page_views) - 1
        if self._page >= last:
            if self._window is not None:
                self._window.close()  # triggers windowWillClose_ → _cleanup
            return
        self._show_page(self._page + 1)

    def _cleanup(self) -> None:
        # Best-effort temp-file removal (delayed so an open Preview isn't affected — here we
        # own the file, so remove it shortly after close).
        tmp = self._qr_tmp
        if tmp:
            self._qr_tmp = None

            def _rm() -> None:
                try:
                    if os.path.exists(tmp):
                        os.unlink(tmp)
                except Exception:
                    pass

            threading.Timer(2.0, _rm).start()
        self._window = None

        # Persist the toggle states on close so ACCEPTED DEFAULTS stick — _on_toggle only
        # fires when the user actively flips a switch, so a pre-checked default the user
        # simply accepts would otherwise never be written. The button state is the source of
        # truth (reflects the default or the last user interaction). No-op for the pairing-only
        # window (no toggles). Then notify the caller so the menu bar can resync.
        for key, btn in self._toggle_buttons:
            try:
                cfg.set_feature(key, bool(btn.state()))
            except Exception as e:
                log.warning("setup wizard: persist %s on close failed: %s", key, e)
        if self._on_finish is not None:
            try:
                self._on_finish()
            except Exception as e:
                log.warning("setup wizard: on_finish callback failed: %s", e)


def present(pair_url: str | None, pairing_only: bool = False,
            on_finish=None) -> SetupWizard | None:
    """Build + show the wizard. `pairing_only=True` shows just the "Pair your iPhone" page
    (the menu's manual "Add device by QR"); the default two-page flow is the first-run intro.
    `on_finish` (if given) is called once the window closes — the menu bar uses it to resync
    its feature-toggle item states with whatever the wizard persisted.

    Returns the retained controller on success (the caller MUST keep the reference alive), or
    None if AppKit window creation failed and the caller should fall back to its plain-alert /
    QR-in-Preview flow. Must be called on the main thread."""
    wiz = SetupWizard(pair_url, pairing_only=pairing_only, on_finish=on_finish)
    if wiz.present():
        return wiz
    return None
