#!/usr/bin/env python3
"""server — The Bridge: always-on ParVagues web dashboard.

A proper webserver (stdlib http.server, same lineage as the Foundry's) serving a
Ship's Bridge dashboard: a live perf/thermal toolbar (folded from perf-tray) plus
an extensible hub of the fleet's tools. Designed to run as a systemd --user
service (parvagues-bridge.service).

    python3 bridge.py serve                 # http://127.0.0.1:8773/
    python3 bridge.py serve --host 0.0.0.0  # opt into LAN (phone)

Binds 127.0.0.1 by DEFAULT: POST /api/perf flips the CPU governor via sudo, so
mode-switching must not be LAN-triggerable unless you say so.

API:
    GET  /api/perf          → perf.snapshot()  (mode, temps, freq, fans, throttle)
    POST /api/perf {mode}   → switch perf mode  → {ok, msg, snapshot}
    GET  /api/hub           → [{name, url, blurb}]  (the tool launcher)
    GET  /api/health        → {ok:true}
"""
from __future__ import annotations

import json
import os
import socket
import sys
from functools import partial
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from urllib.parse import urlparse

HERE = Path(__file__).resolve().parent
sys.path.insert(0, str(HERE))
import perf as P  # noqa: E402

UI = HERE / "ui"

# Hub entries — the fleet's other GUIs. Extensible: add a dict, it shows up as a
# card. url is opened in a new tab; ports match each tool's default serve port.
HUB = [
    {"name": "The Foundry", "url": "http://127.0.0.1:8765/",
     "blurb": "URL → audio → stems → loops (sampling)"},
    {"name": "L'Armada", "url": "http://127.0.0.1:8731/",
     "blurb": "catalog · distribution · gigs (serve.py)"},
]

# One Thermals instance per process → throttle_delta is relative to Bridge start.
_THERM = P.Thermals()


class Handler(BaseHTTPRequestHandler):
    host_is_lan = False

    def do_GET(self):
        p = urlparse(self.path).path
        if p in ("/", ""):
            return self._file(UI / "index.html", "text/html; charset=utf-8")
        if p == "/api/health":
            return self._json({"ok": True})
        if p == "/api/perf":
            return self._json(P.snapshot(_THERM))
        if p == "/api/hub":
            return self._json(HUB)
        # static assets under ui/
        asset = (UI / p.lstrip("/")).resolve()
        if UI in asset.parents and asset.is_file():
            return self._file(asset, self._ctype(asset))
        return self.send_error(404)

    def do_POST(self):
        p = urlparse(self.path).path
        if p == "/api/perf":
            body = self._body()
            mode = (body.get("mode") or "").strip()
            ok, msg = P.set_mode(mode)
            return self._json({"ok": ok, "msg": msg, "snapshot": P.snapshot(_THERM)},
                              200 if ok else 400)
        return self.send_error(404)

    # ── helpers ─────────────────────────────────────────────────────────
    def _body(self):
        n = int(self.headers.get("Content-Length", 0))
        return json.loads(self.rfile.read(n) or b"{}") if n else {}

    def _json(self, obj, code=200):
        b = json.dumps(obj).encode()
        self.send_response(code)
        self.send_header("Content-Type", "application/json")
        self.send_header("Content-Length", str(len(b)))
        self.end_headers()
        self.wfile.write(b)

    def _file(self, path: Path, ctype: str):
        if not path.is_file():
            return self.send_error(404, f"missing {path.name}")
        b = path.read_bytes()
        self.send_response(200)
        self.send_header("Content-Type", ctype)
        self.send_header("Content-Length", str(len(b)))
        self.end_headers()
        self.wfile.write(b)

    @staticmethod
    def _ctype(path: Path):
        return {".css": "text/css", ".js": "text/javascript",
                ".svg": "image/svg+xml", ".png": "image/png"}.get(
            path.suffix, "application/octet-stream")

    def log_message(self, *a):
        pass


def _lan_ip():
    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    try:
        s.connect(("192.168.1.1", 1))
        return s.getsockname()[0]
    finally:
        s.close()


def run(host: str = "127.0.0.1", port: int = 8773):
    httpd = ThreadingHTTPServer((host, port), partial(Handler))
    print(f"⚓ The Bridge — perf mode: {P.detect_mode()}, "
          f"pkg {P.snapshot(_THERM)['package_c']}°C")
    print(f"  http://{'127.0.0.1' if host in ('127.0.0.1','localhost') else host}:{port}/")
    if host == "0.0.0.0":
        try:
            print(f"  LAN: http://{_lan_ip()}:{port}/  ⚠ mode-switch exposed to LAN")
        except OSError:
            pass
    if not P.script_available():
        print(f"  ⓘ mode-switching disabled ({P.SCRIPT} not deployed — see README)")
    try:
        httpd.serve_forever()
    except KeyboardInterrupt:
        print("\n⚓ Bridge docked.")


if __name__ == "__main__":
    import argparse
    ap = argparse.ArgumentParser()
    ap.add_argument("--host", default="127.0.0.1")
    ap.add_argument("--port", type=int, default=8773)
    a = ap.parse_args()
    run(a.host, a.port)
