#!/usr/bin/env python3
"""fed-tools — the Magogi Foundation tools launcher.

  fed-tools list             list every MGF tool
  fed-tools run <web-tool>   install (if needed) + launch a web console
  fed-tools run <t> --show   print the commands instead of running
  fed-tools show <tool>      print any tool's install + run commands
  fed-tools update [tool]    update installed web console(s) to the latest
  fed-tools update --check   only detect what's outdated (exit 1 if any)

Installs with uv when present (the MGF venvs are uv-managed and have no pip),
else falls back to `python -m pip`. Stdlib-only — running THIS needs nothing
installed. See TOOLS_STARTHERE.md (same directory) for the full written
reference. Web tools are runnable today; the CLI tools are listed with their
install + example so you can run them too.
"""
from __future__ import annotations

import argparse
import importlib.metadata as _md
import os
import re
import shutil
import subprocess
import sys
import threading
import urllib.error
import urllib.request
import webbrowser

DEVPI = "https://195.15.203.250.sslip.io/magogi/prod/+simple/"
SRC_DEST = os.path.expanduser("~/.cache/mgf-fed-tools/mgf-cloud-web")


def _color() -> bool:
    return sys.stdout.isatty() and not os.environ.get("NO_COLOR")


_ON = _color()


def _c(code: str) -> str:
    return code if _ON else ""


BOLD, DIM, RED, GRN, YEL, CYN, RST = (
    _c("\033[1m"), _c("\033[2m"), _c("\033[31m"),
    _c("\033[32m"), _c("\033[33m"), _c("\033[36m"), _c("\033[0m"),
)

# --- the tool registry (install/run verified against the repos) -----------------
WEB = {
    "cloud-web": {
        "pkg": "mgf-cloud-web", "console": "mgf-cloud-web", "port": 8787,
        "title": "Cloud console",
        "desc": "Web GUI over mgf-cloud — create + manage Infomaniak/OpenStack VMs, volumes, storage.",
        "source": "https://codeberg.org/magogi-admin/mgf-cloud-web.git",  # off devpi
        "aliases": ("cloud", "cw"),
    },
    "fed-web": {
        "pkg": "mgf-fed-web", "console": "mgf-fed-web", "port": 8788,
        "title": "Federation dashboard (review web)",
        "desc": "Read-mostly cockpit: status, roadmap, in-progress, feedback/review rounds, live activity.",
        "aliases": ("fed", "dashboard", "review", "fw"),
    },
    "test-web": {
        "pkg": "mgf-test-web[web]", "console": "proofwarden", "port": 8789,
        "title": "Test & CI authority console (ProofWarden)",
        "desc": "Single-flight test authority — a test can never run twice at once, whatever the launcher.",
        "aliases": ("proofwarden", "warden", "test", "tw"),
    },
}

# (name, description, example command, pip target)
CLI = [
    ("mgf-fed", "Federation orchestrator (gates, releases, scaffold, registry)", "mgf-fed status", "mgf-fed[cli]"),
    ("fedres", "fed-resources spine + the freshness gate", "fedres check", "mgf-fedres"),
    ("mtest", "Test supervisor — drives pytest, resumable", "mtest --self-check", "mgf-test-supervisor"),
    ("testctl", "Warden CLI — single-flight test runs", "testctl status", "mgf-test-web"),
    ("mgf-cloud", "Cloud resources — VMs, volumes, object storage", "mgf-cloud vm list", "mgf-cloud[infomaniak]"),
    ("mgf-cloud-provision", "Provision self-hosted infra (devpi, CI, Vault)", "mgf-cloud-provision -h", "mgf-cloud-provision[infomaniak]"),
    ("mgf-apiprobe", "REST API verification — typed probes + findings", "apiprobe --help", "mgf-apiprobe[openapi]"),
    ("mgf-hrb", "Hardware resource broker (PCIe, …)", "mgf-hrb info", "mgf-hrb"),
]


def resolve(name: str) -> str | None:
    name = name.lower()
    if name in WEB:
        return name
    return next((k for k, t in WEB.items() if name in t["aliases"]), None)


def install_steps(t: dict) -> list[tuple[str, str]]:
    if "source" in t:
        # the package is off devpi, but its deps (mgf-cloud, …) resolve from devpi
        return [("clone (this one is off devpi)", f"git clone {t['source']} {SRC_DEST}"),
                ("install", f"pip install -e {SRC_DEST} --index-url {DEVPI}")]
    return [("install", f'pip install "{t["pkg"]}" --index-url {DEVPI}')]


def _have(console: str) -> bool:
    return shutil.which(console) is not None


def _dist(t: dict) -> str:
    """The distribution name (pkg without any [extras])."""
    return t["pkg"].split("[")[0]


def _pip(*args: str) -> int:
    """Install via uv when available (the MGF venvs are uv-managed, no pip), else pip."""
    if shutil.which("uv"):
        return _run(["uv", "pip", *args])
    return _run([sys.executable, "-m", "pip", *args])


# --- rendering ------------------------------------------------------------------
def banner() -> None:
    print(f"\n  {CYN}{BOLD}❖ Magogi Foundation — tools{RST}")
    print(f"  {DIM}{'─' * 48}{RST}")


def cmd_list(_args) -> int:
    banner()
    print(f"\n  {BOLD}{CYN}WEB CONSOLES{RST}  {DIM}— fed-tools run <name>{RST}\n")
    for k, t in WEB.items():
        dot = f"{GRN}●{RST}" if _have(t["console"]) else f"{DIM}○{RST}"
        tag = f"{GRN}{DIM}installed{RST}" if _have(t["console"]) else ""
        print(f"    {dot} {GRN}{BOLD}{k:<10}{RST} {t['title']:<42} {DIM}:{t['port']}{RST} {tag}")
        print(f"       {DIM}{t['desc']}{RST}")
    print(f"\n  {BOLD}{CYN}COMMAND-LINE TOOLS{RST}  {DIM}— install + run yourself{RST}\n")
    for name, desc, ex, _pkg in CLI:
        mark = f"{GRN}✓{RST}" if _have(name.split("[")[0]) else f"{DIM}›{RST}"
        print(f"    {mark} {BOLD}{name:<20}{RST} {DIM}{desc}{RST}")
        print(f"       {DIM}e.g.{RST} {YEL}{ex}{RST}")
    print(f"\n  {DIM}Launch a web tool:{RST}  {YEL}fed-tools run fed-web{RST}")
    print(f"  {DIM}See its commands: {RST}  {YEL}fed-tools show fed-web{RST}")
    print(f"  {DIM}Check for updates:{RST}  {YEL}fed-tools update --check{RST}")
    print(f"  {DIM}Full reference:   {RST}  {DIM}fed-tools/TOOLS_STARTHERE.md{RST}\n")
    return 0


def cmd_show(args) -> int:
    k = resolve(args.tool)
    if k:  # a web console: install (+ its steps) then launch, then open the URL
        t = WEB[k]
        url = f"http://127.0.0.1:{t['port']}"
        print(f"\n  {GRN}{BOLD}{t['pkg']}{RST} — {t['title']}  {DIM}→ {url}{RST}\n")
        for label, cmd in install_steps(t) + [("launch", t["console"])]:
            print(f"    {DIM}# {label}{RST}")
            print(f"    {YEL}{cmd}{RST}")
        print(f"\n  {DIM}then open{RST} {CYN}{url}{RST}\n")
        return 0
    row = next((r for r in CLI if r[0] == args.tool.lower()), None)
    if row:  # a command-line tool: install + an example invocation
        name, desc, ex, pkg = row
        print(f"\n  {BOLD}{name}{RST} — {DIM}{desc}{RST}\n")
        print(f"    {DIM}# install{RST}\n    {YEL}pip install \"{pkg}\" --index-url {DEVPI}{RST}")
        print(f"    {DIM}# run{RST}\n    {YEL}{ex}{RST}\n")
        return 0
    return _unknown(args.tool)


def _run(cmd: list[str]) -> int:
    return subprocess.run(cmd).returncode  # noqa: S603


def install(t: dict) -> bool:
    print(f"  {DIM}{t['console']} not found — installing {t['pkg']} …{RST}")
    if "source" in t:
        if not os.path.isdir(SRC_DEST):
            os.makedirs(os.path.dirname(SRC_DEST), exist_ok=True)
            if _run(["git", "clone", t["source"], SRC_DEST]) != 0:
                return False
        return _pip("install", "-e", SRC_DEST, "--index-url", DEVPI) == 0
    return _pip("install", t["pkg"], "--index-url", DEVPI) == 0


# --- update detection + apply ---------------------------------------------------
def _ver_key(v: str) -> tuple:
    return tuple(int(n) for n in re.findall(r"\d+", v)) or (0,)


def _installed_version(dist: str) -> str | None:
    try:
        return _md.version(dist)
    except _md.PackageNotFoundError:
        return None


def _latest_devpi(dist: str) -> str | None:
    """Highest version on the devpi simple index for `dist` (parsed from filenames)."""
    parts = re.escape(dist).replace(r"\-", "[-_.]").replace(r"\_", "[-_.]").replace(r"\.", "[-_.]")
    rx = re.compile(rf"{parts}-([0-9][A-Za-z0-9.]*?)(?:-py|-cp|\.tar|\.zip|\.whl)", re.I)
    try:
        with urllib.request.urlopen(f"{DEVPI}{dist}/", timeout=15) as r:  # noqa: S310
            html = r.read().decode("utf-8", "replace")
    except (urllib.error.URLError, OSError):
        return None
    vs = set(rx.findall(html))
    return max(vs, key=_ver_key) if vs else None


def _git_out(*args: str) -> str | None:
    r = subprocess.run(["git", *args], capture_output=True, text=True)  # noqa: S603, S607
    return r.stdout.strip() if r.returncode == 0 else None


def update_state(t: dict) -> dict:
    """Whether an installed console is outdated. keys: installed, needs, cur, new."""
    if "source" in t:
        if not os.path.isdir(os.path.join(SRC_DEST, ".git")):
            return {"installed": False, "needs": False, "cur": None, "new": None}
        subprocess.run(["git", "-C", SRC_DEST, "fetch", "--quiet"], capture_output=True)  # noqa: S603, S607
        local = _git_out("-C", SRC_DEST, "rev-parse", "HEAD")
        remote = _git_out("-C", SRC_DEST, "rev-parse", "@{u}") \
            or _git_out("-C", SRC_DEST, "rev-parse", "origin/HEAD")
        needs = bool(local and remote and local != remote)
        return {"installed": True, "needs": needs,
                "cur": local[:7] if local else None,
                "new": remote[:7] if remote else None}
    dist = _dist(t)
    cur, new = _installed_version(dist), _latest_devpi(dist)
    needs = bool(cur and new and _ver_key(new) > _ver_key(cur))
    return {"installed": cur is not None, "needs": needs, "cur": cur, "new": new}


def apply_update(t: dict) -> bool:
    if "source" in t:
        if _run(["git", "-C", SRC_DEST, "pull", "--ff-only"]) != 0:
            return False
        return _pip("install", "-e", SRC_DEST, "--index-url", DEVPI) == 0
    return _pip("install", "--upgrade", t["pkg"], "--index-url", DEVPI) == 0


def cmd_update(args) -> int:
    banner()
    targets = WEB
    if args.tool:
        k = resolve(args.tool)
        if not k:
            return _unknown(args.tool)
        targets = {k: WEB[k]}
    print(f"\n  {DIM}checking for updates …{RST}\n")
    outdated = False
    for k, t in targets.items():
        st = update_state(t)
        if not st["installed"]:
            print(f"    {DIM}○ {k:<10} not installed{RST}  {DIM}— fed-tools run {k}{RST}")
            continue
        if st["needs"]:
            outdated = True
            change = f"{st['cur']} {DIM}→{RST} {GRN}{st['new']}{RST}"
            print(f"    {YEL}⟳ {k:<10}{RST} update available  {change}")
            if not args.check:
                ok = apply_update(t)
                print(f"      {GRN}✓ updated{RST}" if ok else f"      {RED}✗ update failed{RST}")
        else:
            print(f"    {GRN}● {k:<10}{RST} up to date  {DIM}({st['cur']}){RST}")
    if args.check:
        print(f"\n  {YEL}updates available{RST}\n" if outdated else f"\n  {GRN}all current{RST}\n")
        return 1 if outdated else 0
    print()
    return 0


def cmd_run(args) -> int:
    k = resolve(args.tool)
    if not k:
        if any(r[0] == args.tool.lower() for r in CLI):
            print(f"  {DIM}{args.tool} is a command-line tool (only web consoles are launchable) — "
                  f"run it directly, or {YEL}fed-tools show {args.tool}{RST} {DIM}for its commands.{RST}")
            return 2
        return _unknown(args.tool)
    t = WEB[k]
    if args.show:
        return cmd_show(args)
    console = t["console"]
    if not _have(console):
        if not install(t):
            print(f"  {RED}install failed — run these manually:{RST}")
            return cmd_show(args)
    url = f"http://127.0.0.1:{t['port']}"
    print(f"\n  {GRN}{BOLD}▶ {t['pkg']}{RST}  →  {CYN}{BOLD}{url}{RST}   {DIM}(Ctrl-C to stop){RST}\n")
    try:
        threading.Timer(2.5, lambda: webbrowser.open(url)).start()
    except Exception:
        pass
    try:
        return subprocess.run([console]).returncode  # noqa: S603
    except KeyboardInterrupt:
        print(f"\n  {DIM}stopped.{RST}")
        return 0
    except FileNotFoundError:
        print(f"  {RED}{console} is still not on PATH.{RST}")
        return cmd_show(args)


def _unknown(name: str) -> int:
    print(f"  {RED}unknown tool: {name}{RST}")
    print(f"  run {YEL}fed-tools list{RST} to see them all "
          f"{DIM}(launchable web tools: {', '.join(WEB)}){RST}")
    return 2


def main(argv=None) -> int:
    p = argparse.ArgumentParser(prog="fed-tools", description="Magogi Foundation tools launcher.")
    sub = p.add_subparsers(dest="cmd")
    sub.add_parser("list", help="list every MGF tool")
    pr = sub.add_parser("run", help="install (if needed) + launch a web console")
    pr.add_argument("tool")
    pr.add_argument("--show", action="store_true", help="print the commands instead of running")
    ps = sub.add_parser("show", help="print any tool's install + run commands")
    ps.add_argument("tool")
    pu = sub.add_parser("update", help="update installed web console(s); --check only detects")
    pu.add_argument("tool", nargs="?", help="one console (default: all installed)")
    pu.add_argument("--check", action="store_true", help="report what's outdated; don't apply")
    args = p.parse_args(argv)
    if args.cmd == "run":
        return cmd_run(args)
    if args.cmd == "show":
        return cmd_show(args)
    if args.cmd == "update":
        return cmd_update(args)
    return cmd_list(args)


if __name__ == "__main__":
    sys.exit(main())
