"""
LaunchAgent plist management — installs/uninstalls the bridge as a login item.
Self-installs on first run so the user never needs to touch launchctl manually.
"""
from __future__ import annotations
import logging
import os
import plistlib
import shutil
import subprocess
import sys
from pathlib import Path

log = logging.getLogger(__name__)

_LABEL = "app.terminull.bridge"
_PLIST_DIR = Path.home() / "Library" / "LaunchAgents"
_PLIST_PATH = _PLIST_DIR / f"{_LABEL}.plist"
_LOG_DIR = Path.home() / "Library" / "Logs" / "Terminull"


_BUNDLE_LAUNCHER = Path.home() / ".terminull" / "Terminull.app" / "Contents" / "MacOS" / "Terminull"


def _binary_path() -> str:
    # Prefer the app bundle launcher (attributes TCC to "Terminull", not "Python").
    if _BUNDLE_LAUNCHER.exists() and os.access(str(_BUNDLE_LAUNCHER), os.X_OK):
        return str(_BUNDLE_LAUNCHER)
    installed = shutil.which("terminull")
    if installed:
        return installed
    return f"{sys.executable} -m terminull_platform.mac.main"


def _bool(b: bool) -> str:
    return "<true/>" if b else "<false/>"


def _plist_content(*, run_at_load: bool = True, keep_alive: bool = True) -> str:
    """Render the plist. The two behaviours are independent:

    - ``run_at_load`` (RunAtLoad)  → start the bridge on login.
    - ``keep_alive``  (KeepAlive)  → relaunch the bridge if it exits *uncleanly*
      (crash / signal — e.g. the SIGKILL a critical-battery hibernate deals out).
      Rendered as ``{Crashed: true, SuccessfulExit: false}`` rather than a bare
      ``true`` **on purpose**: a duplicate instance launched by launchd while the app
      is already running exits **cleanly (0)** via the single-instance guard in
      ``main()``, and a bare-true KeepAlive would then respawn it every
      ThrottleInterval forever (a relaunch storm). Crash-only KeepAlive relaunches a
      real death but leaves the intentional singleton exit alone. Note: a deliberate
      Quit from the menu boots the job out of launchd first (see ``stop``), so Quit
      always sticks regardless of this flag.
    """
    binary = _binary_path()
    _LOG_DIR.mkdir(parents=True, exist_ok=True)
    keepalive_xml = (
        "<dict>\n"
        "        <key>Crashed</key><true/>\n"
        "        <key>SuccessfulExit</key><false/>\n"
        "    </dict>"
        if keep_alive else "<false/>"
    )
    return f"""<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
  "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>{_LABEL}</string>

    <key>ProgramArguments</key>
    <array>
        <string>{binary}</string>
    </array>

    <!-- Relaunch automatically if it crashes/is killed (NOT on a clean singleton exit) -->
    <key>KeepAlive</key>
    {keepalive_xml}

    <!-- Start on login ("Start at Login" toggle) -->
    <key>RunAtLoad</key>
    {_bool(run_at_load)}

    <!-- Logs land in ~/Library/Logs/Terminull/ -->
    <key>StandardOutPath</key>
    <string>{_LOG_DIR}/bridge.log</string>
    <key>StandardErrorPath</key>
    <string>{_LOG_DIR}/bridge.err</string>

    <!-- Give the network a moment before connecting to relay -->
    <key>ThrottleInterval</key>
    <integer>5</integer>
</dict>
</plist>
"""


def is_installed() -> bool:
    return _PLIST_PATH.exists()


def _is_loaded() -> bool:
    """True when the job is actually bootstrapped into the user's GUI launchd domain
    (not merely present on disk). A stale plist that was never loaded — or was booted
    out by a previous Quit and never came back — reads False here."""
    r = subprocess.run(
        ["launchctl", "print", f"gui/{os.getuid()}/{_LABEL}"],
        capture_output=True, text=True, check=False,
    )
    return r.returncode == 0


def install() -> None:
    """Write the plist (keeping it current) and ensure the job is loaded.

    Idempotent and safe to call on every startup: it rewrites the plist only when the
    content actually changed (so it can propagate fixes like the crash-only KeepAlive),
    then bootstraps the job iff it isn't already loaded. Loading while the app is
    already running makes launchd spawn a duplicate that the single-instance guard in
    ``main()`` retires with a clean exit — see ``_plist_content``.
    """
    _PLIST_DIR.mkdir(parents=True, exist_ok=True)
    desired = _plist_content()
    if not _PLIST_PATH.exists() or _PLIST_PATH.read_text() != desired:
        _PLIST_PATH.write_text(desired)
        log.info("LaunchAgent plist written at %s", _PLIST_PATH)
    ensure_loaded()


def ensure_loaded() -> None:
    """Bootstrap the job into launchd if the plist exists but isn't loaded. No-op when
    already loaded (the common case — the shipped app is launched *by* this job)."""
    if not _PLIST_PATH.exists() or _is_loaded():
        return
    _load()
    log.info("LaunchAgent loaded — bridge will start on login and relaunch if it dies")


def uninstall() -> None:
    if not is_installed():
        return
    _unload()
    _PLIST_PATH.unlink(missing_ok=True)
    log.info("LaunchAgent removed")


def _load() -> None:
    subprocess.run(["launchctl", "load", str(_PLIST_PATH)], check=False)


def _unload() -> None:
    subprocess.run(["launchctl", "unload", str(_PLIST_PATH)], check=False)


# ── Per-flag read/write (RunAtLoad + KeepAlive) ──────────────────────────────

def _read_flags() -> tuple[bool, bool]:
    """Return (run_at_load, keep_alive) from the installed plist.

    Defaults to (True, True) — the historical behaviour — when the plist is
    missing or unreadable.
    """
    if not _PLIST_PATH.exists():
        return True, True
    try:
        data = plistlib.loads(_PLIST_PATH.read_bytes())
        return bool(data.get("RunAtLoad", True)), bool(data.get("KeepAlive", True))
    except Exception:
        return True, True


def run_at_login_enabled() -> bool:
    return is_installed() and _read_flags()[0]


def keep_alive_enabled() -> bool:
    return is_installed() and _read_flags()[1]


def _rewrite(*, run_at_load: bool, keep_alive: bool) -> None:
    _PLIST_DIR.mkdir(parents=True, exist_ok=True)
    _PLIST_PATH.write_text(
        _plist_content(run_at_load=run_at_load, keep_alive=keep_alive)
    )


def set_keep_alive(value: bool) -> None:
    """Toggle auto-restart-on-crash. Rewrites the plist preserving RunAtLoad.

    launchd only re-reads the plist at (re)load time, so the change takes effect
    the next time the job is loaded (next login, or after the next Quit, which
    boots the job out). We deliberately do **not** reload here — that would drop
    live PTY sessions.
    """
    run_at_load, _ = _read_flags()
    _rewrite(run_at_load=run_at_load, keep_alive=value)
    log.info("KeepAlive set to %s (applies at next launch)", value)


def stop() -> None:
    """Stop the bridge for this session and keep it stopped.

    ``launchctl bootout`` removes the job from launchd, so KeepAlive can't
    relaunch it — unlike a bare process exit. The plist file stays on disk, so
    the bridge comes back on the next login (if RunAtLoad is set). Falls back to
    ``unload`` on older macOS.
    """
    target = f"gui/{os.getuid()}/{_LABEL}"
    r = subprocess.run(["launchctl", "bootout", target], check=False)
    if r.returncode != 0:
        subprocess.run(["launchctl", "unload", str(_PLIST_PATH)], check=False)


def restart() -> None:
    """Reload the LaunchAgent so the newly installed version starts.

    Calling launchctl unload sends SIGTERM to the current process; launchctl
    load then starts a fresh one with the updated code.
    If the plist isn't installed, fall back to re-exec in place.
    """
    if is_installed():
        subprocess.run(["launchctl", "unload", str(_PLIST_PATH)], check=False)
        subprocess.run(["launchctl", "load",   str(_PLIST_PATH)], check=False)
    else:
        import os
        os.execv(sys.executable, [sys.executable] + sys.argv)


def log_path() -> Path:
    return _LOG_DIR / "bridge.log"


def err_path() -> Path:
    return _LOG_DIR / "bridge.err"
