#!/usr/bin/env python3
"""KateRTX OS pack · multi-tab SPV seats · SDF bus · designer headers as launchers.

Builds:
  out/desktop_os.html   · offline pack with embedded plugs + bus + desktop runtime
  spvs/headers.json     · designer-header catalog (JSON · long details)

Law: BGS→BGF→BGL→SDF · SPV free rides · ESSIE≠EZZIE · never ML
"""
from __future__ import annotations

import json
import re
import time
from html import unescape
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
SPVS = ROOT / "spvs"
CSS = ROOT / "css"
JS = ROOT / "js"
OUT = ROOT / "out"
ICONS = ROOT / "icons"


def _json_script(obj) -> str:
    return (
        json.dumps(obj, ensure_ascii=False)
        .replace("<", "\\u003c")
        .replace(">", "\\u003e")
        .replace("&", "\\u0026")
        .replace("\u2028", "\\u2028")
        .replace("\u2029", "\\u2029")
    )


def _strip_tags(s: str) -> str:
    s = re.sub(r"<[^>]+>", " ", s or "")
    s = unescape(s)
    s = re.sub(r"\s+", " ", s).strip()
    return s


def parse_designer_header(dom: str, plug: dict) -> dict:
    """Extract SPV designer header → launcher icon metadata + long details."""
    pid = plug.get("id", "?")
    title = plug.get("title") or pid
    subtitle = ""
    glyph = plug.get("icon") or "·"
    header_html = ""
    header_class = ""

    # first <header>...</header>
    m = re.search(r"<header\b([^>]*)>(.*?)</header>", dom or "", re.I | re.S)
    if m:
        header_class = (re.search(r'class=["\']([^"\']+)', m.group(1) or "") or [None, ""])[1]
        header_html = m.group(0)
        body = m.group(2)
        for pat in (
            r'class=["\'][^"\']*spv-title[^"\']*["\'][^>]*>(.*?)</',
            r'class=["\'][^"\']*mail-app-title[^"\']*["\'][^>]*>(.*?)</',
            r"<h[12][^>]*>(.*?)</h[12]>",
            r"<strong[^>]*>(.*?)</strong>",
            r'class=["\'][^"\']*spv-grin-mark[^"\']*["\'][^>]*>(.*?)</',
        ):
            tm = re.search(pat, body, re.I | re.S)
            if tm:
                t = _strip_tags(tm.group(1))
                if t:
                    title = t
                    break
        for pat in (
            r'class=["\'][^"\']*meta[^"\']*["\'][^>]*>(.*?)</',
            r'class=["\'][^"\']*mail-app-sub[^"\']*["\'][^>]*>(.*?)</',
            r'class=["\'][^"\']*mute[^"\']*["\'][^>]*>(.*?)</',
        ):
            sm = re.search(pat, body, re.I | re.S)
            if sm:
                subtitle = _strip_tags(sm.group(1))
                if subtitle:
                    break
        gm = re.search(
            r'class=["\'][^"\']*(?:mail-app-icon|spv-grin-mark|app-icon)[^"\']*["\'][^>]*>(.*?)</',
            body,
            re.I | re.S,
        )
        if gm:
            g = _strip_tags(gm.group(1))
            if g and len(g) <= 4:
                glyph = g
        # title attribute on icon
        tm = re.search(r'title=["\']([^"\']+)["\']', body)
        if tm and not subtitle:
            subtitle = tm.group(1)

    # fallback title from first .spv-title outside header
    if title == pid or title == plug.get("title"):
        tm = re.search(
            r'class=["\'][^"\']*spv-title[^"\']*["\'][^>]*>(.*?)</',
            dom or "",
            re.I | re.S,
        )
        if tm:
            t = _strip_tags(tm.group(1))
            if t:
                title = t

    rides = plug.get("rides") or "BGF"
    details = {
        "id": pid,
        "title": title,
        "subtitle": subtitle,
        "glyph": glyph,
        "icon": plug.get("icon") or glyph,
        "header_class": header_class or "",
        "header_present": bool(header_html),
        "designer": True,
        "launcher": True,
        "rides": rides,
        "ground": "BGS",
        "place": "SDF",
        "secure": bool(plug.get("secure")),
        "desktop": plug.get("desktop", True) is not False,
        "essie": bool(plug.get("essie")),
        "utility": bool(plug.get("utility") or plug.get("secure")),
        "product": bool(plug.get("product") or plug.get("home")),
        "system": bool(plug.get("system")),
        "family": plug.get("family") or plug.get("proto") or plug.get("atom") or "",
        "proto": plug.get("proto"),
        "port": plug.get("port"),
        "theme": plug.get("theme") or "dark",
        "html5": plug.get("html5"),
        "note": plug.get("note") or subtitle,
        "bus": {
            "default_mode": "secure" if plug.get("secure") else "insecure",
            "lanes": ["BGF", "BGS"],
            "speak": "SDF",
            "broadcast_ok": True,
        },
        "law": "SPV free rides BGF · never folds into SDF · header is launcher face",
        "stack": "BGS ground → BGF measure → BGL secure → SDF place",
        "source": {
            "js": plug.get("js"),
            "dom": plug.get("dom"),
        },
        "long": (
            f"{title} · SPV plug `{pid}` · designer header is the OS icon/launcher face. "
            f"Rides {rides} on ground BGS · place SDF. "
            f"{subtitle or plug.get('note') or 'hotswap seat via ESSIE'}. "
            f"Inter-tab speak uses SDF bus modes insecure|secure|broadcast."
        ),
    }
    return {
        "id": pid,
        "title": title,
        "subtitle": subtitle,
        "glyph": glyph,
        "icon": plug.get("icon") or glyph,
        "header_class": header_class or "",
        "designer": True,
        "details": details,
    }


def load_manifest() -> dict:
    p = SPVS / "manifest.json"
    return json.loads(p.read_text(encoding="utf-8"))


def ensure_santa(man: dict) -> dict:
    """Guarantee santa plug + mail alias in manifest for pack."""
    ids = {p["id"] for p in man.get("plugs", [])}
    if "santa" not in ids and (SPVS / "santa.js").exists() and (SPVS / "santa.dom.html").exists():
        man.setdefault("plugs", []).insert(
            2,
            {
                "id": "santa",
                "js": "santa.js",
                "dom": "santa.dom.html",
                "title": "Santa Mail",
                "icon": "🎅",
                "rides": "BGF",
                "essie": True,
                "desktop": True,
                "mail": True,
                "secure": True,
                "theme": "dark",
                "note": "red·white·green · Titan · ESSIE hotswap · replaces krtx-mail",
            },
        )
    return man


def build_blocks(man: dict) -> tuple[list[dict], list[dict]]:
    headers = []
    blocks = []
    # santa before mail so alias resolves when scripts inject
    plugs = list(man.get("plugs") or [])
    plugs.sort(key=lambda p: (0 if p.get("id") == "santa" else 1 if p.get("id") == "mail" else 2, p.get("id") or ""))

    for p in plugs:
        dom_p = SPVS / p["dom"]
        js_p = SPVS / p["js"]
        if not dom_p.exists() or not js_p.exists():
            continue
        dom = dom_p.read_text(encoding="utf-8", errors="replace")
        js = js_p.read_text(encoding="utf-8", errors="replace")
        header = parse_designer_header(dom, p)
        headers.append(header)
        blocks.append(
            {
                "id": p["id"],
                "title": header["title"],
                "subtitle": header.get("subtitle") or "",
                "icon": header["icon"],
                "glyph": header["glyph"],
                "theme": p.get("theme", "dark"),
                "dom": dom,
                "js": js,
                "rides": p.get("rides", "BGF"),
                "family": p.get("family") or p.get("proto") or p.get("atom") or "",
                "secure": bool(p.get("secure")),
                "desktop": p.get("desktop", True) is not False,
                "utility": bool(p.get("utility") or p.get("secure") and p.get("id") == "antivirus"),
                "essie": bool(p.get("essie")),
                "product": bool(p.get("product") or p.get("home")),
                "system": bool(p.get("system")),
                "header": header,
                "details": header.get("details"),
                "bus_default": "secure" if p.get("secure") else "insecure",
            }
        )
    return blocks, headers


def read_css() -> str:
    parts = []
    for name in ("spv.css", "shell.css"):
        p = CSS / name
        if not p.exists():
            continue
        txt = p.read_text(encoding="utf-8")
        if name == "shell.css":
            txt = txt.replace('@import url("spv.css");', "/* spv.css inlined above */")
        parts.append(f"/* === {name} === */\n{txt}")
    return "\n".join(parts)


def build() -> Path:
    OUT.mkdir(parents=True, exist_ok=True)
    man = ensure_santa(load_manifest())
    # write santa into manifest if missing
    man_path = SPVS / "manifest.json"
    man["version"] = man.get("version") or "0.8.0"
    man["os"] = {
        "name": "KateRTX OS",
        "runtime": "js/desktop.js",
        "bus": "js/bus.js",
        "tabs": "multi-seat concurrent SPV",
        "bus_modes": ["insecure", "secure", "broadcast"],
        "lanes": ["BGF", "BGS"],
        "protocol": "SDF",
        "headers": "designer headers = icons + launchers",
        "json": True,
        "long_details": True,
    }
    man_path.write_text(json.dumps(man, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")

    blocks, headers = build_blocks(man)
    catalog = {
        "name": "KateRTX designer headers",
        "version": "1.0.0",
        "generated": time.strftime("%Y-%m-%dT%H:%M:%S"),
        "law": "designer header face = OS icon + launcher · SPV free rides BGF",
        "count": len(headers),
        "bus": {
            "protocol": "SDF",
            "lanes": ["BGS", "BGF"],
            "modes": ["insecure", "secure", "broadcast"],
            "place": "SDF",
            "ride": "BGF",
            "ground": "BGS",
        },
        "headers": headers,
        "by_id": {h["id"]: h for h in headers},
        "details": {
            "purpose": (
                "Each SPV DOM <header> is the designer face of the app. "
                "KateRTX OS uses that face as the desktop icon and Start-menu launcher. "
                "Tabs keep one live seat per open SPV; seats speak SDF on the bus."
            ),
            "selection": "Click icon or Start item → open/focus multi-seat tab",
            "speak": "/bus insecure|secure|broadcast · /lane BGF|BGS · seats speak JSON frames",
        },
    }
    (SPVS / "headers.json").write_text(
        json.dumps(catalog, indent=2, ensure_ascii=False) + "\n", encoding="utf-8"
    )

    bus_js = (JS / "bus.js").read_text(encoding="utf-8")
    desktop_js = (JS / "desktop.js").read_text(encoding="utf-8")
    commands_js = (
        (JS / "commands.js").read_text(encoding="utf-8")
        if (JS / "commands.js").exists()
        else "/* no commands.js */"
    )
    css = read_css()
    stamp = time.strftime("%Y%m%d-%H%M%S")

    html = f"""<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>KateRTX OS · multi-tab SPV · SDF bus</title>
  <meta name="description" content="KateRTX OS · SPV per tab · SDF on BGS/BGF · insecure secure broadcast" />
  <style>
{css}
  </style>
</head>
<body class="desk-fullscreen mode-online bus-mode-insecure bus-lane-bgf">
  <div id="wallpaper" aria-hidden="true"></div>

  <div class="tabbar" id="tabbar">
    <button type="button" class="start-btn" id="start-btn" title="Start · SPV launchers">
      <span class="start-mark">E</span>
    </button>
    <div class="tab-row" id="tab-row" role="tablist" aria-label="SPV seats"></div>
    <div class="tab-tools tabbar-end">
      <span id="hud-pill" class="hud-pill">SPV · multi-seat</span>
      <div class="bus-bar raised-sdf" id="bus-bar" title="SDF bus mode + lane">
        <span class="bus-label">SDF</span>
        <button type="button" class="bus-mode-btn on" data-mode="insecure" title="Insecure · open">INSEC</button>
        <button type="button" class="bus-mode-btn" data-mode="secure" title="Secure · token ring">SEC</button>
        <button type="button" class="bus-mode-btn" data-mode="broadcast" title="Broadcast · all seats">BC</button>
        <span class="bus-sep">|</span>
        <button type="button" class="bus-lane-btn on" data-lane="BGF" title="BGF measure">BGF</button>
        <button type="button" class="bus-lane-btn" data-lane="BGS" title="BGS ground">BGS</button>
      </div>
      <button type="button" class="net-btn net-toggle on" id="net-toggle" data-mode="online">Online</button>
    </div>
  </div>

  <div id="start-menu" class="start-menu raised-sdf" aria-label="Start">
    <div class="start-head fly-head">
      <strong>ESSIE · designer headers</strong>
      <span>icons = SPV headers · launch seats</span>
    </div>
    <div id="fly-plugs" class="fly-plugs"></div>
  </div>

  <div class="desk-body">
    <main id="stage" class="stage stage-surface" aria-live="polite">
      <!-- multi-seat panes injected by desktop.js · home icons when empty -->
    </main>
    <aside class="desk-chat raised-sdf" id="desk-chat" id="chat">
      <div class="chat-log" id="chat-log"></div>
      <form id="chat-form" class="chat-form" autocomplete="off">
        <input id="chat-in" type="text" placeholder="/list · /in term · /bus secure · /lane BGF" />
        <button type="submit">↵</button>
      </form>
    </aside>
  </div>

  <div id="ctx-menu" class="ctx-menu raised-sdf" hidden>
    <div class="ctx-head">KateRTX OS</div>
    <div id="ctx-list" class="ctx-list"></div>
  </div>

  <script>
    window.__BGRTX_PACK__ = {_json_script(blocks)};
    window.__BGRTX_START_TABS__ = [];
    window.__BGRTX_HEADERS__ = {_json_script(catalog)};
    window.__BGRTX_ICON_BASE__ = "../icons/";
    window.__BGRTX_OS__ = {_json_script(man.get("os") or {})};
    window.__BGRTX_PACK_STAMP__ = {_json_script(stamp)};
  </script>
  <script>
{bus_js}
  </script>
  <script>
{commands_js}
  </script>
  <script>
{desktop_js}
  </script>
</body>
</html>
"""
    # fix duplicate id on chat aside
    html = html.replace('id="desk-chat" id="chat"', 'id="desk-chat"')

    out = OUT / "desktop_os.html"
    out.write_text(html, encoding="utf-8")
    # also stamp copy
    stamped = OUT / f"desktop_os-{stamp}.html"
    stamped.write_text(html, encoding="utf-8")
    print("PACK", out)
    print("PACK", stamped)
    print("HEADERS", SPVS / "headers.json", "count", len(headers))
    print("PLUGS", len(blocks))
    return out


if __name__ == "__main__":
    build()
