"""Live GUI preview server: render an app's JSON Schema as an RJSF form.

No Node/build step — the shared @acellera/pm-rjsf renderer (the same one pmview
uses) is loaded over a CDN in the browser, so the preview matches production.
This closes the authoring loop: edit signature -> refresh -> see the form ->
copy inputs.json.

The renderer is the BUNDLED preview build (``dist/pm-rjsf.preview.js``: MUI /
emotion / @rjsf / ajv bundled in, only React external), pulled raw from jsdelivr
and wired to a single React instance via an importmap. We deliberately do NOT
load the externalized ``dist/pm-rjsf.js`` from a CDN: esm.sh resolves its peer
deps into duplicate React/MUI instances, which silently breaks hooks, MUI
context (accordions won't expand) and reactive ``ui:disabled``.

Local-build preview: set ``PM_RJSF_LOCAL=1`` (or ``PM_RJSF_LOCAL=/path/to/
pm-rjsf.preview.js``) to serve a locally built bundle instead of the published
jsdelivr copy. Run ``npm run dev:preview`` in the pm-rjsf checkout to rebuild on
save, then refresh.
"""

from __future__ import annotations

import json
import os
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer

from ._generate import func_to_entry, signature_to_inputs_model

# Published @acellera/pm-rjsf version. Must be a release that ships the BUNDLED
# preview build (dist/pm-rjsf.preview.js) -- see _PM_RJSF_URL below.
_PM_RJSF_VERSION = "0.0.5"

# Both modes load the renderer via bare specifiers resolved by an importmap, so
# there is a single React instance shared with this page's createRoot.
_IMPORTS = """import React from "react";
import { createRoot } from "react-dom/client";
import { PMForm } from "@acellera/pm-rjsf";"""


def _importmap(pm_rjsf_url):
    """Importmap shared by both modes; ``pm_rjsf_url`` is the renderer source.

    The renderer is always the BUNDLED preview build (MUI / emotion / @rjsf /
    ajv bundled in, only React external), so the only bare specifiers left to
    resolve are React (+ react-dom, shared with this page's createRoot for a
    single React instance), react/jsx-runtime and the inert @mui/material/colors
    constants rollup leaves external. We must NOT load the externalized
    dist/pm-rjsf.js over a CDN: esm.sh resolves its peer deps (@mui, react,
    emotion) into duplicate React/MUI instances, which silently breaks hooks,
    MUI context (accordions won't expand) and reactive ui:disabled. Versions
    pinned to the build's deps.
    """
    return {
        "imports": {
            "react": "https://esm.sh/react@18.3.1",
            "react/jsx-runtime": "https://esm.sh/react@18.3.1/jsx-runtime",
            "react-dom": "https://esm.sh/react-dom@18.3.1?external=react",
            "react-dom/client": "https://esm.sh/react-dom@18.3.1/client?external=react",
            "@mui/material/colors": "https://esm.sh/@mui/material@5.18.0/colors",
            "@acellera/pm-rjsf": pm_rjsf_url,
        }
    }


# Default (CDN) mode: the bundled preview build, served RAW by jsdelivr (no
# re-processing, so its bare React import resolves via the importmap above).
_PM_RJSF_URL = (
    f"https://cdn.jsdelivr.net/npm/@acellera/pm-rjsf@{_PM_RJSF_VERSION}"
    "/dist/pm-rjsf.preview.js"
)

_PREVIEW_HTML = """<!doctype html>
<html><head><meta charset="utf-8"><title>pmmanifest GUI preview</title>
<style>
 /* 416px = pmview's left-panel width (buildLayoutJson size), so the preview form
    matches the real app's width. */
 body{font-family:system-ui,sans-serif;max-width:416px;margin:2rem auto;padding:0 1rem;color:#333}
 pre{background:#f5f5f5;padding:1rem;border-radius:8px;overflow:auto}
 .err{color:#b00}
 /* On-screen error log: surfaces console/runtime/render errors so you don't have
    to open DevTools. Hidden until something is logged. */
 #errbox{display:none;white-space:pre-wrap;background:#2b0000;color:#ff9a9a;
   border:1px solid #b00;border-radius:8px;padding:.6rem .9rem;margin:1rem 0;
   font-family:ui-monospace,Menlo,monospace;font-size:.82rem;line-height:1.4}
 #errbox b{color:#ffd2d2}
 /* Form + grouping CSS is injected by @acellera/pm-rjsf (PMForm). */
</style>
<script>
 // Capture errors BEFORE the module loads and mirror them into #errbox. Buffer
 // anything logged before the body exists, then flush on DOMContentLoaded.
 window.__pmErrQ = [];
 window.__pmErr = function (kind, msg) {
   var box = document.getElementById("errbox");
   if (!box) { window.__pmErrQ.push([kind, msg]); return; }
   box.style.display = "block";
   var line = document.createElement("div");
   line.innerHTML = "<b></b>";
   line.firstChild.textContent = kind + ": ";
   line.appendChild(document.createTextNode(String(msg)));
   box.appendChild(line);
 };
 addEventListener("DOMContentLoaded", function () {
   var q = window.__pmErrQ.splice(0);
   q.forEach(function (p) { window.__pmErr(p[0], p[1]); });
 });
 addEventListener("error", function (ev) {
   window.__pmErr("error", ev.message + (ev.filename ? " (" + ev.filename + ":" + ev.lineno + ")" : ""));
 });
 addEventListener("unhandledrejection", function (ev) {
   window.__pmErr("promise", (ev.reason && ev.reason.stack) || ev.reason);
 });
 var _consoleError = console.error.bind(console);
 console.error = function () {
   try {
     window.__pmErr("console.error", Array.prototype.map.call(arguments, function (a) {
       return a && a.stack ? a.stack : (typeof a === "object" ? JSON.stringify(a) : a);
     }).join(" "));
   } catch (e) { /* never let logging throw */ }
   _consoleError.apply(console, arguments);
 };
</script>
__PM_RJSF_IMPORTMAP__
</head>
<body>
<h2 id="title">Loading...</h2>
<div id="errbox"></div>
<p>Live RJSF preview of the app's JSON Schema. Submit to see the resulting
<code>inputs.json</code> (also validated server-side by the Pydantic model).</p>
<div id="root"></div>
<h3>Resulting inputs.json</h3>
<pre id="out">-</pre>
<script type="module">
__PM_RJSF_IMPORTS__

const e = React.createElement;
const data = await (await fetch("/schema")).json();
document.getElementById("title").textContent = data.name + " - GUI preview";
const out = document.getElementById("out");
function onSubmit({ formData }) {
  out.textContent = JSON.stringify(formData, null, 2);
  fetch("/inputs", { method: "POST", body: JSON.stringify(formData) })
    .then(r => r.json())
    .then(v => { if (!v.ok) out.innerHTML += '\\n\\n<span class="err">server validation: '
                                              + v.error + "</span>"; });
}

// Grouping panels, map fields, file widgets, "?" tooltips and field titles are
// all handled by @acellera/pm-rjsf's templates + transforms, applied inside
// PMForm -- so we hand it the raw schema and uiSchema.
try {
  createRoot(document.getElementById("root")).render(
    e(PMForm, { schema: data.schema, uiSchema: data.uiSchema, onSubmit }));
} catch (err) {
  window.__pmErr("render", (err && err.stack) || err);
  document.getElementById("root").innerHTML = '<pre class="err">' + err + "</pre>";
}
</script>
</body></html>"""


def _resolve_local_dist():
    """Return the local pm-rjsf bundle path if ``PM_RJSF_LOCAL`` enables it, else None.

    ``PM_RJSF_LOCAL=1`` uses the bundled preview build (``npm run dev:preview`` ->
    ``dist/pm-rjsf.preview.js``); set it to an explicit path to override.
    """
    raw = os.environ.get("PM_RJSF_LOCAL")
    if raw in (None, "", "0", "false"):
        return None
    if raw in ("1", "true"):
        return os.path.expanduser("~/Work/pm-rjsf/dist/pm-rjsf.preview.js")
    return os.path.expanduser(raw)


def _render_html(local_dist):
    # Local mode serves its bundled build at /pm-rjsf.js; CDN mode pulls the
    # published bundled build from jsdelivr. Both go through the same importmap.
    pm_rjsf_url = "/pm-rjsf.js" if local_dist else _PM_RJSF_URL
    importmap = '<script type="importmap">' + json.dumps(_importmap(pm_rjsf_url)) + "</script>"
    return _PREVIEW_HTML.replace("__PM_RJSF_IMPORTMAP__", importmap).replace(
        "__PM_RJSF_IMPORTS__", _IMPORTS
    )


def serve(func, *, port: int = 8000, function: str | None = None, name: str | None = None):
    """Serve a live RJSF preview of ``func``'s generated JSON Schema on ``port``.

    ``GET /schema`` returns ``{name, schema, uiSchema}``; ``GET /`` serves the
    page; ``POST /inputs`` validates submitted form data with the synthesized
    Pydantic model (proving coercion + PMPath flexibility). In local-build mode
    ``GET /pm-rjsf.js`` serves the locally built renderer.
    """
    entry = func_to_entry(func, function=function)
    app_name = name or getattr(func, "__name__", "app")
    payload = {"name": app_name, "schema": entry["inputSchema"], "uiSchema": entry["uiSchema"]}
    model, _ = signature_to_inputs_model(func)
    local_dist = _resolve_local_dist()

    class _Handler(BaseHTTPRequestHandler):
        def log_message(self, *_args):
            pass

        def _send(self, code, body, ctype):
            data = body.encode() if isinstance(body, str) else body
            self.send_response(code)
            self.send_header("Content-Type", ctype)
            self.send_header("Content-Length", str(len(data)))
            self.end_headers()
            self.wfile.write(data)

        def do_GET(self):
            if self.path == "/":
                self._send(200, _render_html(local_dist), "text/html")
            elif self.path == "/schema":
                self._send(200, json.dumps(payload), "application/json")
            elif local_dist and self.path == "/pm-rjsf.js":
                try:
                    with open(local_dist, "rb") as fh:
                        self._send(200, fh.read(), "application/javascript")
                except OSError as ex:  # noqa: BLE001 - surface a readable error
                    self._send(500, f"cannot read {local_dist}: {ex}", "text/plain")
            else:
                self._send(404, "not found", "text/plain")

        def do_POST(self):
            n = int(self.headers.get("Content-Length", 0))
            raw = self.rfile.read(n).decode() or "{}"
            try:
                model.model_validate(json.loads(raw))
                self._send(200, json.dumps({"ok": True}), "application/json")
            except Exception as ex:  # noqa: BLE001 - surface any validation error
                self._send(
                    200,
                    json.dumps({"ok": False, "error": str(ex).splitlines()[0]}),
                    "application/json",
                )

    print(f"pmmanifest: serving GUI preview on http://127.0.0.1:{port}  (Ctrl-C to stop)")
    if local_dist:
        print(f"pmmanifest: local @acellera/pm-rjsf -> {local_dist}")
    ThreadingHTTPServer(("127.0.0.1", port), _Handler).serve_forever()
