#!/usr/bin/env python3
"""
Bootstrap compiler: js.sx (self-hosting SX-to-JS translator) → sx-browser.js.

This is the canonical JS bootstrapper. js.sx is loaded into the Python evaluator,
which uses it to translate the .sx spec files into JavaScript. Platform code
(types, primitives, DOM interface) comes from platform_js.py.

Usage:
    python run_js_sx.py                                      # stdout
    python run_js_sx.py -o shared/static/scripts/sx-browser.js  # file
"""
from __future__ import annotations

import os
import sys

_HERE = os.path.dirname(os.path.abspath(__file__))
_PROJECT = os.path.abspath(os.path.join(_HERE, "..", ".."))
if _PROJECT not in sys.path:
    sys.path.insert(0, _PROJECT)

import tempfile
from shared.sx.parser import serialize
from hosts.javascript.platform import (
    extract_defines,
    ADAPTER_FILES, ADAPTER_DEPS, SPEC_MODULES, SPEC_MODULE_ORDER, EXTENSION_NAMES,
    PREAMBLE, PLATFORM_JS_PRE, PLATFORM_JS_POST,
    PRIMITIVES_JS_MODULES, _ALL_JS_MODULES, _assemble_primitives_js,
    PLATFORM_DEPS_JS, PLATFORM_PARSER_JS, PLATFORM_DOM_JS,
    PLATFORM_ENGINE_PURE_JS, PLATFORM_ORCHESTRATION_JS, PLATFORM_BOOT_JS,
    PLATFORM_CEK_JS, CEK_FIXUPS_JS,
    CONTINUATIONS_JS, ASYNC_IO_JS,
    fixups_js, public_api_js, EPILOGUE,
)


_bridge = None  # cached OcamlSync instance


def _get_bridge():
    """Get or create the OCaml sync bridge with transpiler loaded."""
    global _bridge
    if _bridge is not None:
        return _bridge
    from shared.sx.ocaml_sync import OcamlSync
    _bridge = OcamlSync()
    _bridge.load(os.path.join(_HERE, "transpiler.sx"))
    return _bridge


def load_js_sx():
    """Load js.sx transpiler into the OCaml kernel. Returns the bridge."""
    return _get_bridge()


def compile_ref_to_js(
    adapters: list[str] | None = None,
    modules: list[str] | None = None,
    extensions: list[str] | None = None,
    spec_modules: list[str] | None = None,
) -> str:
    """Compile SX spec files to JavaScript using js.sx.

    Args:
        adapters: List of adapter names to include. None = all.
        modules: List of primitive module names. None = all.
        extensions: List of extensions (continuations). None = none.
        spec_modules: List of spec modules (deps, router, signals). None = auto.
    """
    from datetime import datetime, timezone

    # Source directories: core spec, standard library, web framework
    _source_dirs = [
        os.path.join(_PROJECT, "spec"),      # Core language spec
        os.path.join(_PROJECT, "lib"),       # Standard library (stdlib, compiler, vm, ...)
        os.path.join(_PROJECT, "web"),       # Web framework
    ]
    bridge = _get_bridge()

    # Resolve adapter set
    if adapters is None:
        adapter_set = set(ADAPTER_FILES.keys())
    else:
        adapter_set = set()
        for a in adapters:
            if a not in ADAPTER_FILES:
                raise ValueError(f"Unknown adapter: {a!r}. Valid: {', '.join(ADAPTER_FILES)}")
            adapter_set.add(a)
            for dep in ADAPTER_DEPS.get(a, []):
                adapter_set.add(dep)

    # Resolve spec modules
    spec_mod_set = set()
    if spec_modules:
        for sm in spec_modules:
            if sm not in SPEC_MODULES:
                raise ValueError(f"Unknown spec module: {sm!r}. Valid: {', '.join(SPEC_MODULES)}")
            spec_mod_set.add(sm)
    if "dom" in adapter_set and "signals" in SPEC_MODULES:
        spec_mod_set.add("signals")
        if "signals-web" in SPEC_MODULES:
            spec_mod_set.add("signals-web")
    if "boot" in adapter_set:
        spec_mod_set.add("router")
        spec_mod_set.add("deps")
        if "page-helpers" in SPEC_MODULES:
            spec_mod_set.add("page-helpers")
    # CEK is always included (part of evaluator.sx core file)
    has_cek = True
    has_deps = "deps" in spec_mod_set
    has_router = "router" in spec_mod_set
    has_page_helpers = "page-helpers" in spec_mod_set

    # Resolve extensions
    ext_set = set()
    if extensions:
        for e in extensions:
            if e not in EXTENSION_NAMES:
                raise ValueError(f"Unknown extension: {e!r}. Valid: {', '.join(EXTENSION_NAMES)}")
            ext_set.add(e)
    has_continuations = "continuations" in ext_set

    # Build file list: core evaluator + adapters + spec modules
    # evaluator.sx = merged frames + eval utilities + CEK machine
    sx_files = [
        ("evaluator.sx", "evaluator (frames + eval + CEK)"),
        # stdlib.sx is loaded at runtime via eval, not transpiled —
        # transpiling it would shadow native PRIMITIVES in module scope.
        ("freeze.sx", "freeze (serializable state boundaries)"),
        ("content.sx", "content (content-addressed computation)"),
        ("render.sx", "render (core)"),
        ("web-forms.sx", "web-forms (defstyle, deftype, defeffect, defrelation)"),
    ]
    for name in ("parser", "html", "sx", "dom-lib", "browser-lib", "dom", "engine", "orchestration", "boot"):
        if name in adapter_set:
            sx_files.append(ADAPTER_FILES[name])
    # Use explicit ordering for spec modules (respects dependencies)
    for name in SPEC_MODULE_ORDER:
        if name in spec_mod_set:
            sx_files.append(SPEC_MODULES[name])
    # Any spec modules not in the order list (future-proofing)
    for name in sorted(spec_mod_set):
        if name not in SPEC_MODULE_ORDER:
            sx_files.append(SPEC_MODULES[name])

    has_html = "html" in adapter_set
    has_sx = "sx" in adapter_set
    has_dom = "dom" in adapter_set
    has_engine = "engine" in adapter_set
    has_orch = "orchestration" in adapter_set
    has_boot = "boot" in adapter_set
    has_parser = "parser" in adapter_set
    has_signals = "signals" in spec_mod_set
    adapter_label = "+".join(sorted(adapter_set)) if adapter_set else "core-only"

    # Platform JS blocks keyed by adapter name
    adapter_platform = {
        "parser":        PLATFORM_PARSER_JS,
        "dom":           PLATFORM_DOM_JS,
        "engine":        PLATFORM_ENGINE_PURE_JS,
        "orchestration": PLATFORM_ORCHESTRATION_JS,
        "boot":          PLATFORM_BOOT_JS,
    }

    # Determine primitive modules
    prim_modules = None
    if modules is not None:
        prim_modules = [m for m in _ALL_JS_MODULES if m.startswith("core.")]
        for m in modules:
            if m not in prim_modules:
                if m not in PRIMITIVES_JS_MODULES:
                    raise ValueError(f"Unknown module: {m!r}. Valid: {', '.join(PRIMITIVES_JS_MODULES)}")
                prim_modules.append(m)

    # Build output
    parts = []
    parts.append(PREAMBLE)
    parts.append(PLATFORM_JS_PRE)
    parts.append('\n  // =========================================================================')
    parts.append('  // Primitives')
    parts.append('  // =========================================================================\n')
    parts.append('  var PRIMITIVES = {};')
    parts.append(_assemble_primitives_js(prim_modules))
    parts.append(PLATFORM_JS_POST)

    if has_deps:
        parts.append(PLATFORM_DEPS_JS)

    if has_parser:
        parts.append(adapter_platform["parser"])

    # CEK platform aliases must come before transpiled cek.sx (which uses them)
    if has_cek:
        parts.append(PLATFORM_CEK_JS)

    # Translate each spec file using js.sx
    def _find_sx(filename):
        for d in _source_dirs:
            p = os.path.join(d, filename)
            if os.path.exists(p):
                return p
        return None

    for filename, label in sx_files:
        filepath = _find_sx(filename)
        if not filepath:
            continue
        with open(filepath) as f:
            src = f.read()
        defines = extract_defines(src)

        sx_defines = [[name, expr] for name, expr in defines]

        parts.append(f"\n  // === Transpiled from {label} ===\n")
        # Serialize defines to SX, write to temp file, load into OCaml kernel
        defines_sx = serialize(sx_defines)
        with tempfile.NamedTemporaryFile(mode="w", suffix=".sx", delete=False) as tmp:
            tmp.write(f"(define _defines \'{defines_sx})\n")
            tmp_path = tmp.name
        try:
            bridge.load(tmp_path)
        finally:
            os.unlink(tmp_path)
        result = bridge.eval("(js-translate-file _defines)")
        parts.append(result)

    # Platform JS for selected adapters
    if not has_dom:
        parts.append("\n  var _hasDom = false;\n")

    # CEK fixups + general fixups BEFORE boot (boot hydrates islands that need these)
    parts.append(fixups_js(has_html, has_sx, has_dom, has_signals, has_deps, has_page_helpers))
    if has_cek:
        parts.append(CEK_FIXUPS_JS)

    # Load stdlib.sx via eval (NOT transpiled) so defines go into the eval
    # env, not the module scope. This prevents stdlib functions from
    # shadowing native PRIMITIVES aliases used by transpiled evaluator code.
    stdlib_path = _find_sx("stdlib.sx")
    if stdlib_path:
        with open(stdlib_path) as f:
            stdlib_src = f.read()
        # Escape for JS string literal
        stdlib_escaped = stdlib_src.replace("\\", "\\\\").replace('"', '\\"').replace("\n", "\\n")
        parts.append(f'\n  // === stdlib.sx (eval\'d at runtime, not transpiled) ===')
        parts.append(f'  (function() {{')
        parts.append(f'    var src = "{stdlib_escaped}";')
        parts.append(f'    var forms = sxParse(src);')
        parts.append(f'    var tmpEnv = merge({{}}, PRIMITIVES);')
        parts.append(f'    for (var i = 0; i < forms.length; i++) {{')
        parts.append(f'      trampoline(evalExpr(forms[i], tmpEnv));')
        parts.append(f'    }}')
        parts.append(f'    for (var k in tmpEnv) {{')
        parts.append(f'      if (!PRIMITIVES[k]) PRIMITIVES[k] = tmpEnv[k];')
        parts.append(f'    }}')
        parts.append(f'  }})();\n')

    for name in ("dom", "engine", "orchestration", "boot"):
        if name in adapter_set and name in adapter_platform:
            parts.append(adapter_platform[name])
    # CONTINUATIONS_JS is the tree-walk shift/reset extension.
    # With CEK as sole evaluator, continuations are handled natively by
    # cek.sx (step-sf-reset, step-sf-shift). Skip the tree-walk extension.
    # if has_continuations:
    #     parts.append(CONTINUATIONS_JS)
    if has_dom:
        parts.append(ASYNC_IO_JS)
    parts.append(public_api_js(has_html, has_sx, has_dom, has_engine, has_orch, has_boot, has_parser, adapter_label, has_deps, has_router, has_signals, has_page_helpers, has_cek))
    parts.append(EPILOGUE)

    build_ts = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
    return "\n".join(parts).replace("BUILD_TIMESTAMP", build_ts)


if __name__ == "__main__":
    import argparse
    p = argparse.ArgumentParser(description="Bootstrap-compile SX reference spec to JavaScript via js.sx")
    p.add_argument("--adapters", "-a",
                    help="Comma-separated adapter list (html,sx,dom,engine). Default: all")
    p.add_argument("--modules", "-m",
                    help="Comma-separated primitive modules (core.* always included). Default: all")
    p.add_argument("--extensions",
                    help="Comma-separated extensions (continuations). Default: none.")
    p.add_argument("--spec-modules",
                    help="Comma-separated spec modules (deps). Default: none.")
    default_output = os.path.join(_HERE, "..", "..", "static", "scripts", "sx-browser.js")
    p.add_argument("--output", "-o", default=default_output,
                    help="Output file (default: shared/static/scripts/sx-browser.js)")
    args = p.parse_args()

    adapters = args.adapters.split(",") if args.adapters else None
    modules = args.modules.split(",") if args.modules else None
    extensions = args.extensions.split(",") if args.extensions else None
    spec_modules = args.spec_modules.split(",") if args.spec_modules else None
    js = compile_ref_to_js(adapters, modules, extensions, spec_modules)

    with open(args.output, "w") as f:
        f.write(js)
    included = ", ".join(adapters) if adapters else "all"
    mods = ", ".join(modules) if modules else "all"
    ext_label = ", ".join(extensions) if extensions else "none"
    print(f"Wrote {args.output} ({len(js)} bytes, adapters: {included}, modules: {mods}, extensions: {ext_label})",
          file=sys.stderr)
