"""Sparkle auto-update integration via PyObjC.

This module exposes a thin wrapper around `SPUStandardUpdaterController` —
Sparkle's recommended entry point in 2.x. We instantiate one controller at
app launch and expose two operations:

  - `init_updater()`   — call once from app startup. Loads Sparkle.framework
                         from .app/Contents/Frameworks, starts the controller
                         with autoCheck=True so it fires the first scheduled
                         check after SUScheduledCheckInterval (set in
                         Info.plist via setup.py).

  - `check_for_updates_now()` — fires Sparkle's "check now" dialog
                         immediately. Wired to the rumps menu item
                         "Check for Updates…" so the user can manually
                         look for fixes between scheduled checks.

The actual update logic — download, verify against SUPublicEDKey, present
the install prompt, launch the auto-update helper, relaunch the .app —
all lives in Sparkle. We're just calling its public API.

LOADING NOTES
-------------
Sparkle.framework is bundled at .app/Contents/Frameworks/Sparkle.framework
by scripts/build_app.sh (post-py2app step). At runtime, PyObjC needs to be
told to load it explicitly because pyobjc's startup scan only looks at
system framework paths; the bundled framework lives under the app's own
Frameworks dir.

When running from an unbundled `python -m src.app` (dev path), the
framework isn't on disk anywhere PyObjC will find it — `init_updater()`
detects that situation, logs a notice, and is a no-op. Dev workflow is
unaffected; auto-update only activates in the shipped .app.
"""

from __future__ import annotations

import os
import sys
from pathlib import Path

import objc  # type: ignore
from Foundation import NSBundle  # type: ignore


_controller = None  # SPUStandardUpdaterController instance, kept alive globally


def _find_sparkle_framework() -> Path | None:
    """Locate Sparkle.framework either inside the running .app bundle
    (production) or anywhere relative to the project root (dev). Returns
    None if not found — caller treats that as 'feature disabled'."""
    # 1) Production: running from inside Lirelle.app — main bundle's Frameworks
    main_bundle = NSBundle.mainBundle()
    if main_bundle is not None:
        bundle_path = main_bundle.bundlePath()
        if bundle_path:
            candidate = Path(str(bundle_path)) / "Contents" / "Frameworks" / "Sparkle.framework"
            if candidate.exists():
                return candidate
    # 2) Dev: project root has vendor/Sparkle.framework
    here = Path(__file__).resolve().parent.parent
    candidate = here / "vendor" / "Sparkle.framework"
    if candidate.exists():
        return candidate
    return None


def init_updater() -> None:
    """Load Sparkle and start the standard updater. Idempotent — repeated
    calls are no-ops. Safe to call before the rumps run loop starts; Sparkle
    schedules its own NSTimer for the periodic check."""
    global _controller
    if _controller is not None:
        return

    framework_path = _find_sparkle_framework()
    if framework_path is None:
        print("[updater] Sparkle.framework not found — auto-update disabled "
              "(this is expected in `python -m src.app` dev runs)")
        return

    bundle = NSBundle.bundleWithPath_(str(framework_path))
    if bundle is None:
        print(f"[updater] failed to create bundle for {framework_path}")
        return
    if not bundle.load():
        print(f"[updater] Sparkle bundle present but failed to load — "
              f"check that {framework_path}/Sparkle is signed and arch-matched")
        return

    # objc.lookUpClass throws if the class isn't yet registered (i.e. the
    # framework didn't load fully). We caught that above with bundle.load(),
    # so this should always succeed; the try-except is belt-and-suspenders.
    try:
        SPUStandardUpdaterController = objc.lookUpClass("SPUStandardUpdaterController")
    except objc.error as e:
        print(f"[updater] Sparkle loaded but SPUStandardUpdaterController missing: {e}")
        return

    # Init signature in Sparkle 2.x:
    #   initWithStartingUpdater:updaterDelegate:userDriverDelegate:
    # startingUpdater=True means "start the updater immediately on init,
    # using the schedule from Info.plist (SUScheduledCheckInterval)". No
    # delegates needed for vanilla behavior — we'd add one only if we
    # wanted custom UI for the "update available" prompt.
    _controller = SPUStandardUpdaterController.alloc()\
        .initWithStartingUpdater_updaterDelegate_userDriverDelegate_(
            True, None, None,
        )

    if _controller is None:
        print("[updater] SPUStandardUpdaterController init returned nil")
        return

    print(f"[updater] Sparkle started — feed URL from Info.plist, "
          f"public EdDSA key from Info.plist, EdDSA private key from Keychain")


def check_for_updates_now() -> None:
    """Fire the 'check for updates' user flow now. If Sparkle isn't loaded
    (dev run, framework missing, framework failed to load), prints a notice
    and returns — never raises so a wired menu item is always safe to click."""
    global _controller
    if _controller is None:
        print("[updater] check requested but Sparkle isn't initialised — "
              "are you running from a frozen .app?")
        return
    updater = _controller.updater()
    if updater is None:
        print("[updater] controller has no updater attached")
        return
    # checkForUpdates presents Sparkle's standard "checking…" panel,
    # then either "no updates available" or the "install now / later" prompt.
    # Sparkle 2.x: zero-arg method, PyObjC selector resolves to checkForUpdates()
    # (the old 1.x `checkForUpdates:` with `_` underscore variant raises
    # `'SPUUpdater' object has no attribute 'checkForUpdates_'` — caught by
    # the outer try/except in app.py._menu_check_updates, which is what
    # made the manual menu item silently no-op on v0.1.0–v0.1.6).
    updater.checkForUpdates()
