"""launchers — the fleet's app launchers, shared by the web Bridge and the tray.

An allow-listed registry (never arbitrary commands): each entry knows how to be
found, whether it's running, and how to spawn detached. Both faces — the web hub
(`/api/launch`) and the perf-tray menu — drive the same `launch(key)`.

"Open if running, else launch": on Wayland there's no portable window-focus, so
a running app reports `already-running` (the user can click again knowingly)
rather than spawning a duplicate.
"""
from __future__ import annotations

import os
import shutil
import socket
import subprocess
from pathlib import Path

HERE = Path(__file__).resolve().parent
TIDAL = Path.home() / "Work" / "Sound" / "Tidal"
ARDOUR_SESSION = Path.home() / "Work" / "Sound" / "Ardour" / "Tidal Multi" / "Tidal Multi.ardour"
MIDIMON = HERE / "midimon.py"

# Terminal emulators we know how to wrap, best first.
TERMINALS = ("konsole", "kitty", "alacritty", "gnome-terminal", "x-terminal-emulator")


def which_first(candidates):
    for c in candidates:
        p = shutil.which(c)
        if p:
            return p
    return None


def _term_argv(inner):
    """Wrap an argv to run inside the first available terminal emulator."""
    term = which_first(TERMINALS)
    if not term:
        return None
    base = os.path.basename(term)
    if base == "gnome-terminal":
        return [term, "--", *inner]
    if base == "kitty":
        return [term, *inner]
    return [term, "-e", *inner]            # konsole, alacritty, x-terminal-emulator


# key -> spec. `candidates` = binaries (first found wins); `args` appended;
# `pgrep` = running-check pattern; `terminal` = run inside a terminal emulator.
LAUNCHERS = [
    {"key": "pulsar", "name": "Pulsar", "blurb": "editor — open the Tidal folder",
     "candidates": ["pulsar"], "args": [str(TIDAL)], "pgrep": "pulsar"},
    {"key": "ardour", "name": "Ardour", "blurb": "DAW — the Tidal Multi session",
     "candidates": ["ardour8", "ardour7", "ardour6", "ardour"],
     "args": ([str(ARDOUR_SESSION)] if ARDOUR_SESSION.exists() else []), "pgrep": "ardour"},
    {"key": "qjackctl", "name": "QjackCtl", "blurb": "JACK / audio graph control",
     "candidates": ["qjackctl"], "args": [], "pgrep": "qjackctl"},
    {"key": "midimon", "name": "MIDI Monitor", "blurb": "live, parsed MIDI seq (aseqdump done right)",
     "candidates": ["python3"], "args": [str(MIDIMON)], "pgrep": "midimon.py", "terminal": True},
]

# Web tools = start-or-open. `port` open ⇒ running; else `cmd` is spawned, then
# the UI opens `url`. (Fixes dead hub links to not-yet-started servers.)
WEB = [
    {"key": "foundry", "name": "The Foundry", "blurb": "URL → audio → stems → loops",
     "url": "http://127.0.0.1:8765/", "port": 8765,
     "cmd": ["python3", str(TIDAL / "tools/foundry/server.py"), "--port", "8765"]},
    # Serve tide-table (the catalog viz lives there) and open triangle.html —
    # exactly what `tide.py serve` does. Serving armada/ root = a dir listing.
    {"key": "armada", "name": "L'Armada", "blurb": "catalog · triangle viz · gigs",
     "url": "http://127.0.0.1:8731/triangle.html", "port": 8731,
     "cmd": ["python3", str(TIDAL / "armada/serve.py"),
             "--dir", str(TIDAL / "armada/tide-table"), "--port", "8731"]},
]

BY_KEY = {l["key"]: l for l in (*LAUNCHERS, *WEB)}


def _port_open(port, host="127.0.0.1"):
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        s.settimeout(0.25)
        return s.connect_ex((host, port)) == 0


def binary(spec):
    return which_first(spec["candidates"])


def available(spec):
    if not binary(spec):
        return False
    if spec.get("terminal") and not which_first(TERMINALS):
        return False
    return True


def is_running(spec):
    try:
        return subprocess.run(["pgrep", "-f", spec["pgrep"]],
                              capture_output=True).returncode == 0
    except OSError:
        return False


def _argv(spec):
    inner = [binary(spec), *spec.get("args", [])]
    return _term_argv(inner) if spec.get("terminal") else inner


def launch(key):
    """Spawn the launcher detached. Returns {ok, status, msg, url?}."""
    spec = BY_KEY.get(key)
    if not spec:
        return {"ok": False, "status": "unknown", "msg": f"unknown launcher {key!r}"}

    if "port" in spec:                                   # web service: start-or-open
        if _port_open(spec["port"]):
            return {"ok": True, "status": "already-running", "url": spec["url"],
                    "msg": f"{spec['name']} is up"}
        try:
            subprocess.Popen(spec["cmd"], cwd=str(TIDAL), start_new_session=True,
                             stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
        except OSError as e:
            return {"ok": False, "status": "error", "msg": str(e)}
        return {"ok": True, "status": "launched", "url": spec["url"],
                "msg": f"starting {spec['name']}…"}

    if not available(spec):
        return {"ok": False, "status": "unavailable",
                "msg": f"{spec['name']} not installed ({'/'.join(spec['candidates'])})"}
    if not spec.get("terminal") and is_running(spec):
        # Wayland: no portable focus — report rather than duplicate.
        return {"ok": True, "status": "already-running",
                "msg": f"{spec['name']} is already running"}
    argv = _argv(spec)
    if not argv:
        return {"ok": False, "status": "no-terminal", "msg": "no terminal emulator found"}
    try:
        subprocess.Popen(argv, cwd=str(TIDAL), start_new_session=True,
                         stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
    except OSError as e:
        return {"ok": False, "status": "error", "msg": str(e)}
    return {"ok": True, "status": "launched", "msg": f"launched {spec['name']}"}


def snapshot():
    """Apps + web tools with live state for the UI / tray."""
    apps = [{"key": s["key"], "name": s["name"], "blurb": s["blurb"], "kind": "app",
             "available": available(s), "running": is_running(s),
             "terminal": bool(s.get("terminal"))} for s in LAUNCHERS]
    web = [{"key": s["key"], "name": s["name"], "blurb": s["blurb"], "kind": "web",
            "url": s["url"], "available": True, "running": _port_open(s["port"])}
           for s in WEB]
    return {"web": web, "apps": apps}
