"""Start/stop the Vite dev server from the web UI.

Dev-only convenience: production serves the built interface/web/dist from this
same backend, but hot-reload editing of the frontend needs Vite running
separately. Shares management/ncp.py's PID file (runtime/pids/frontend.pid)
and process patterns so `ncp status` / `ncp stop --frontend` see the same
process regardless of which side started it.
"""
from __future__ import annotations

import os
import shutil
import subprocess
import urllib.request

try:
    import psutil
except ImportError:  # optional in the portable/Termux core install
    psutil = None

from . import proc_util
from .nexus_config import FRONTEND_SOURCE_DIR, RUNTIME_DIR

FRONTEND_DIR = FRONTEND_SOURCE_DIR
PID_FILE = RUNTIME_DIR / "pids" / "frontend.pid"
LOG_FILE = RUNTIME_DIR / "frontend.log"
PORT = 5173


def _npm() -> str | None:
    return shutil.which("npm.cmd" if os.name == "nt" else "npm")


def _read_pid() -> int | None:
    try:
        return int(PID_FILE.read_text().strip())
    except (OSError, ValueError):
        return None


def _is_ours(pid: int) -> bool:
    # On Windows, npm.cmd can't run as a CreateProcess image directly, so the
    # OS wraps it as `cmd.exe /c ...npm.cmd run dev ...` - the ".cmd" splits
    # "npm" from "run dev", so a literal "npm run dev" substring never matches
    # the tracked PID's own cmdline. Loosen to "vite" (matches once the tree
    # gets that far) or "npm"+"dev" both present (matches the wrapper hop too).
    try:
        if psutil is not None:
            cmd = " ".join(psutil.Process(pid).cmdline())
        else:
            cmd = proc_util.pid_cmdline(pid)
    except Exception:
        return False
    return "vite" in cmd or ("npm" in cmd and "dev" in cmd)


def _alive(pid: int | None) -> bool:
    if pid is None:
        return False
    if psutil is not None:
        return psutil.pid_exists(pid)
    # proc_util rather than os.kill(pid, 0): same probe, but it also works
    # when the PID belongs to another user.
    return proc_util.pid_alive(pid)


def _http_up() -> bool:
    try:
        with urllib.request.urlopen(f"http://127.0.0.1:{PORT}/", timeout=1.5) as r:
            return r.status == 200
    except Exception:
        return False


def is_running() -> bool:
    """Mirrors ncp.py's own status check: PID liveness first, then whether the
    port actually answers (covers Vite started by another process, or a lost
    PID file) - not the stricter pattern match `stop()` uses before killing."""
    pid = _read_pid()
    if _alive(pid):
        return True
    return _http_up()


def start() -> dict:
    if is_running():
        return {"status": "already_running"}
    if not FRONTEND_DIR.is_dir():
        return {"status": "unavailable", "detail": "Vite source is not included in wheel installs"}
    npm = _npm()
    if not npm:
        return {"status": "error", "detail": "npm not found - install Node.js"}

    argv = [npm, "run", "dev", "--", "--host", "0.0.0.0"]
    PID_FILE.parent.mkdir(parents=True, exist_ok=True)
    LOG_FILE.write_text("")
    # CREATE_NO_WINDOW (not DETACHED_PROCESS): a detached child has no console
    # of its own, so Windows hands one to any console program it spawns in
    # turn - npm.cmd -> node -> vite would each flash a window. CREATE_NO_WINDOW
    # gives it a console that's never shown, inherited down the chain.
    kwargs = ({"creationflags": subprocess.CREATE_NEW_PROCESS_GROUP
               | getattr(subprocess, "CREATE_NO_WINDOW", 0)}
              if os.name == "nt" else {"start_new_session": True})
    with open(LOG_FILE, "ab") as log:
        proc = subprocess.Popen(argv, cwd=str(FRONTEND_DIR), stdout=log,
                                 stderr=subprocess.STDOUT, stdin=subprocess.DEVNULL,
                                 **kwargs)
    PID_FILE.write_text(str(proc.pid))
    return {"status": "started", "pid": proc.pid}


def stop() -> dict:
    pid = _read_pid()
    if _alive(pid) and _is_ours(pid):
        try:
            if psutil is not None:
                proc = psutil.Process(pid)
                # npm.cmd -> node -> vite is a multi-hop tree; kill it depth-first
                # so the parent doesn't outlive its children as an orphaned shell.
                for child in proc.children(recursive=True):
                    try:
                        child.terminate()
                    except Exception:
                        pass
                proc.terminate()
            else:
                proc_util.terminate_pid(pid)
        except Exception:
            pass
    PID_FILE.unlink(missing_ok=True)
    return {"status": "stopped"}
