#!/usr/bin/env python3
"""Open the NexusOS UI in a native window.

Uses pywebview, which renders via the WebView2 runtime on Windows (already
present on Win10/11) -- a real app window with no browser chrome and none of the
Edge --app profile cold-start. Blocks until the window is closed; the launcher
waits on this process and stops the services when it exits.

The window opens IMMEDIATELY on a loading page and navigates to the app once the
backend answers, rather than waiting up to 40s with nothing on screen and then
appearing. One window the whole time: nothing pops up and vanishes, and there is
no moment where the user is left wondering whether the command did anything.
The loading page's status line is updated live (memory -> backend -> ready) via
evaluate_js, instead of sitting on one static sentence for the whole wait - the
point is a user watching the window can tell what stage it's stuck on, without
needing to go read a terminal or a log file to find out.

Falls back to the default browser if pywebview/WebView2 is unavailable, staying
alive so the launcher doesn't tear the services down underneath it.
"""
import os
import sys
import time
import urllib.request

URL = "http://localhost:8000"
MEMORY_URL = "http://localhost:8001/"
ICON_PATH = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "assets", "NexusOS.ico")

# Inline so it renders with no server and no asset files - the whole point is
# that it shows before anything is listening. Colours match the app's dark UI so
# the swap to the real page is not a flash of a different-looking window. The
# <p id> is the hook _set_status() rewrites as boot moves through its stages.
LOADING_HTML = """<!doctype html>
<html><head><meta charset="utf-8"><title>NexusOS</title><style>
  html,body{height:100%;margin:0}
  body{background:#14161a;color:#e6e8ec;display:flex;align-items:center;
       justify-content:center;font-family:Segoe UI,system-ui,sans-serif}
  .box{text-align:center}
  .ring{width:44px;height:44px;margin:0 auto 22px;border-radius:50%;
        border:3px solid #2a2f38;border-top-color:#4c8dff;
        animation:spin 1s linear infinite}
  @keyframes spin{to{transform:rotate(360deg)}}
  h1{font-size:17px;font-weight:600;margin:0 0 6px}
  p{font-size:13px;color:#8b93a1;margin:0}
</style></head><body>
  <div class="box">
    <div class="ring"></div>
    <h1>Starting NexusOS</h1>
    <p id="status">Starting memory service...</p>
  </div>
</body></html>"""

FAILED_HTML = LOADING_HTML.replace(
    "<div class=\"ring\"></div>", ""
).replace(
    "Starting NexusOS", "Backend did not start"
).replace(
    'id="status">Starting memory service...',
    'id="status">Nothing answered on :8000 after 40s. Check: ncp logs -b',
)


def _set_status(win, text: str) -> None:
    # Best-effort: the window can be mid-teardown (user closed it while this
    # background thread was still polling), and evaluate_js on a dead window
    # raises - that's not a reason to kill the polling loop.
    try:
        win.evaluate_js(f"document.getElementById('status').textContent = {text!r}")
    except Exception:
        pass


def _http_ok(url: str, timeout: float = 2.0) -> bool:
    try:
        with urllib.request.urlopen(url, timeout=timeout) as r:
            return r.status == 200
    except Exception:
        return False


def _wait_for_backend(win, timeout: float = 40.0) -> bool:
    """Poll memory, then the backend, updating the on-screen status as each
    comes up - so the window never loads before the server is up (which shows
    a localhost error the webview won't retry), and never sits on one sentence
    while actually moving through two separate services."""
    status_url = URL.rstrip("/") + "/status"
    deadline = time.time() + timeout

    memory_ready = _http_ok(MEMORY_URL)
    if memory_ready:
        _set_status(win, "Starting backend...")

    while time.time() < deadline:
        if not memory_ready:
            memory_ready = _http_ok(MEMORY_URL)
            if memory_ready:
                _set_status(win, "Starting backend...")

        try:
            # 5s, not 2: /status probes Ollama, and a wedged Ollama made it
            # slower than a 2s ceiling - the backend was up and answering 200
            # while this loop timed out on every attempt and declared it dead.
            # The server side is fixed too; this is the margin.
            with urllib.request.urlopen(status_url, timeout=5) as r:
                if r.status == 200:
                    _set_status(win, "Opening Nexus...")
                    return True
        except Exception:
            pass
        time.sleep(0.5)
    return False


def main() -> int:
    try:
        import webview
    except Exception as e:  # pywebview not installed
        return _browser_fallback(f"pywebview unavailable ({e})")

    try:
        window = webview.create_window(
            "NexusOS",
            html=LOADING_HTML,
            width=1200,
            height=800,
            min_size=(900, 600),
        )

        def _swap_in_app(win):
            """Runs once the GUI loop is up, so the spinner is already on screen
            while we wait. load_url replaces the loading page in place - there is
            never a second window to close."""
            if _wait_for_backend(win):
                win.load_url(URL)
            else:
                print("[nexus] backend not reachable on :8000 after 40s.",
                      file=sys.stderr)
                win.load_html(FAILED_HTML)

        icon = ICON_PATH if os.path.isfile(ICON_PATH) else None
        webview.start(_swap_in_app, window, icon=icon)  # blocks until the window is closed
        return 0
    except Exception as e:  # no WebView2 runtime / backend failure
        return _browser_fallback(f"native window failed ({e})")


def _browser_fallback(reason: str) -> int:
    import webbrowser

    print(f"[nexus] {reason}; opening default browser instead.", file=sys.stderr)
    webbrowser.open(URL)
    # Stay alive so the launcher keeps the services up. The user stops NexusOS
    # by closing the launcher (or the browser tab, then the services idle out).
    try:
        while True:
            time.sleep(3600)
    except KeyboardInterrupt:
        return 0


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