/**
 * Code generation: lower a `.wrn` AST to TypeScript that targets the framework's
 * existing primitives.
 *
 *   state              -> a `data-scope` declaration consumed by the runtime
 *   view               -> an HTML string returned by a page component
 *   @event="..."       -> data-on-<event>="..."
 *   "...{expr}..."     -> text kept verbatim ({expr} is mustache for runtime)
 *   api="<name>"       -> SSR/client data binding declared in a mode block
 *   ssrGet/ssrText     -> legacy server-side API fetch + render
 *   csrGet/csrText     -> legacy browser-side API fetch + render
 *   style              -> tagged local stylesheet metadata promoted by SSR
 *   functions          -> server-only helpers for API/realtime code
 *   api M /p {b}       -> export const M = async (ctx) => { b }
 *   realtime {..}      -> export const websocket = { evt(ws, ...args) { b } }
 */

import { Buffer } from "node:buffer";

import { VOID_ELEMENTS, type Attr, type DataMode, type PageAst, type ViewNode } from "./parser.ts";
import { eraseFunctionTypes, runtimeTypeOf } from "./types.ts";
import { stripRuntimeFunctionModifiers } from "@wrnexus/syntax";
import { generateStoreModule } from "./store-codegen.ts";
import { optimizeAst } from "./analysis.ts";
import { browserModuleRequired } from "./client-codegen.ts";

interface RenderBinding {
  method: string;
  path: string;
  body: string;
  helpers: string;
}

interface SsrBinding extends RenderBinding {
  marker: string;
}

interface CsrBinding extends RenderBinding {
  id: string;
}

interface NamedDataBinding extends RenderBinding {
  mode: DataMode;
}

function isComponentTag(tag: string): boolean {
  return /^[A-Z][A-Za-z0-9_$]*$/.test(tag);
}

const HTML_BOOLEAN_ATTRIBUTES = new Set([
  "allowfullscreen",
  "async",
  "autofocus",
  "autoplay",
  "checked",
  "controls",
  "default",
  "defer",
  "disabled",
  "formnovalidate",
  "hidden",
  "inert",
  "ismap",
  "itemscope",
  "loop",
  "multiple",
  "muted",
  "nomodule",
  "novalidate",
  "open",
  "playsinline",
  "readonly",
  "required",
  "reversed",
  "selected",
]);

function isHtmlBooleanAttribute(name: string): boolean {
  return HTML_BOOLEAN_ATTRIBUTES.has(name.toLowerCase());
}

const URL_ATTRIBUTES = new Set([
  "href",
  "src",
  "action",
  "formaction",
  "poster",
  "cite",
  "background",
  "xlink:href",
]);

function stripAsciiControlAndSpace(value: string): string {
  let result = "";
  for (const character of value) {
    if (character.charCodeAt(0) > 0x20) result += character;
  }
  return result;
}

function sanitizeUrlAttribute(value: string): string {
  const compact = stripAsciiControlAndSpace(value.trim());
  const lower = compact.toLowerCase();
  if (/^(?:javascript|vbscript|file):/.test(lower)) return "about:blank";
  if (/^data:(?!image\/(?:png|gif|jpeg|webp|avif);)/.test(lower)) return "about:blank";
  return value;
}

function safeAttributeValue(name: string, value: string): string {
  if (!URL_ATTRIBUTES.has(name.toLowerCase()) || value.includes("{")) return value;
  return sanitizeUrlAttribute(value);
}

/** Escape a value placed inside a double-quoted HTML attribute. */
function attrEscape(value: string): string {
  return value
    .replace(/&/g, "&amp;")
    .replace(/"/g, "&quot;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;");
}

/** Make HTML safe to embed inside a JS template literal. */
function templateEscape(html: string): string {
  return html.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$\{/g, "\\${");
}

function styleEscape(css: string): string {
  return css.replace(/<\/style/gi, "<\\/style");
}

function attrValue(attrs: Attr[], name: string): string | undefined {
  return attrs.find((attr) => !attr.event && attr.name === name)?.value;
}

function renderAttr(attr: Attr): string {
  if (attr.event) return ` ${eventAttribute(attr.name)}="${attrEscape(attr.value)}"`;

  switch (attr.name) {
    case "api":
    case "ssrGet":
    case "ssrText":
    case "csrGet":
    case "csrText":
      return "";
    default:
      return attr.boolean
        ? ` ${attr.name}`
        : ` ${attr.name}="${attrEscape(safeAttributeValue(attr.name, attr.value))}"`;
  }
}

function eventAttribute(name: string): string {
  if (name.startsWith("window:")) {
    return `data-on-window-${name.slice("window:".length)}`;
  }

  if (name.startsWith("document:")) {
    return `data-on-document-${name.slice("document:".length)}`;
  }

  if (name.startsWith("browser-")) {
    return `data-on-wrnexus-browser-${name.slice(8)}`;
  }

  if (name.startsWith("mobile-")) {
    return `data-on-wrnexus-mobile-${name.slice(7)}`;
  }

  return `data-on-${name}`;
}

/**
 * Event attribute for a handler written on a *component tag*
 * (`<Modal @confirm="save()">`).
 *
 * These need their own attribute name. The mount's attributes are forwarded
 * into the component and land on its view root, i.e. inside the component's
 * own `data-scope` -- but the statement (`save()`) belongs to the parent that
 * wrote the tag. Emitting `data-on-confirm` makes the child's runtime bind it
 * against the child's scope, where the parent's functions and state do not
 * exist, so the handler silently does nothing. `data-wrn-out-*` is ignored by
 * the child and claimed by the mounting scope instead.
 *
 * `window:`/`document:` (and the browser/mobile bridges) keep the plain
 * `data-on-*` form: those bind to a global target rather than to the element,
 * and the runtime has no component-output path for them.
 */
function componentEventAttribute(name: string): string {
  if (
    name.startsWith("window:") ||
    name.startsWith("document:") ||
    name.startsWith("browser-") ||
    name.startsWith("mobile-")
  ) {
    return eventAttribute(name);
  }

  return `data-wrn-out-${name}`;
}

function reactiveAttrValue(raw: string, reactive: PageReactive): string | null {
  let found = false;
  const value = raw.replace(/\{([^{}]+)\}/g, (whole, inner: string) => {
    const expr = inner.trim();
    if (!exprRefsState(expr, reactive.stateNames)) return whole;
    found = true;
    try {
      const result = new Function("with(this){return (" + expr + ");}").call(reactive.scope);
      return result == null ? "" : String(result);
    } catch {
      return whole;
    }
  });
  return found ? value : null;
}

function renderAttrs(
  attrs: Attr[],
  csrId?: string,
  reactive: PageReactive | null = null,
  dynamicExpressions?: string[],
): string {
  let bindIndex = 0;
  const rendered = attrs
    .map((attr) => {
      const base = renderAttr(attr);
      if (!reactive || attr.event || attr.boolean || !base || !attr.value.includes("{"))
        return base;
      const expression = wholeAttributeExpression(attr.value);
      if (
        expression &&
        exprRefsState(expression, reactive.runtimeStateNames) &&
        dynamicExpressions
      ) {
        dynamicExpressions.push(`\${__wrnexusPropAttr(${expression})}`);
        const sentinel = `\x00WRNEACH${dynamicExpressions.length - 1}\x00`;
        const marker = JSON.stringify([attr.name, attr.value]);
        return ` ${attr.name}="${sentinel}" data-wrn-bind-${bindIndex++}="${attrEscape(marker)}"`;
      }
      const initial = reactiveAttrValue(attr.value, reactive);
      if (initial === null) return base;
      const marker = JSON.stringify([attr.name, attr.value]);
      return ` ${attr.name}="${attrEscape(URL_ATTRIBUTES.has(attr.name.toLowerCase()) ? sanitizeUrlAttribute(initial) : initial)}" data-wrn-bind-${bindIndex++}="${attrEscape(marker)}"`;
    })
    .join("");
  return csrId ? `${rendered} data-wrnexus-csr="${attrEscape(csrId)}"` : rendered;
}

/**
 * Replace i18n text sugar `{t:key}` with a `<span data-t="key">` marker the
 * runtime resolves server-side. Other `{expr}` mustaches are left untouched.
 */
function substituteTMarkers(text: string): string {
  return text.replace(
    /\{t:([^{}]+)\}/g,
    (_m, key: string) => `<span data-t="${attrEscape(key.trim())}"></span>`,
  );
}

/** Escape a value for safe embedding in HTML text. */
function htmlTextEscape(value: string): string {
  return value.replace(/[&<>]/g, (c) => (c === "&" ? "&amp;" : c === "<" ? "&lt;" : "&gt;"));
}

/** Reactive page context: state names + their initial (SSR) values. */
interface PageReactive {
  stateNames: Set<string>;
  runtimeStateNames: Set<string>;
  scope: Record<string, unknown>;
}

/**
 * Evaluate a page's `state` seed expressions at compile time to obtain the
 * initial SSR values used to bake `data-text` spans. Seeds may reference
 * earlier ones; anything that can't be evaluated becomes `undefined`.
 */
function evalStateSeeds(states: { name: string; expr: string }[]): Record<string, unknown> {
  const scope: Record<string, unknown> = {};
  for (const s of states) {
    try {
      scope[s.name] = new Function("with(this){return (" + s.expr + ");}").call(scope);
    } catch {
      scope[s.name] = undefined;
    }
  }
  return scope;
}

/**
 * Page text compilation: resolve `{t:key}` i18n markers, then bake state
 * interpolations (`{count}`, `{count * 2}`) into `data-text` spans carrying the
 * evaluated initial value — so no-JS clients see real content and the reactive
 * runtime keeps it live. Non-state `{expr}` and un-evaluable expressions are
 * left as literal client mustaches.
 */
function substituteReactiveText(
  raw: string,
  reactive: PageReactive | null,
  dynamicExpressions?: string[],
): string {
  const text = substituteTMarkers(raw);
  if (!reactive || reactive.stateNames.size === 0) return text;
  return text.replace(/\{([^{}]+)\}/g, (whole, inner: string) => {
    const expr = inner.trim();
    if (expr.startsWith("t:") || !exprRefsState(expr, reactive.stateNames)) return whole;
    if (exprRefsState(expr, reactive.runtimeStateNames) && dynamicExpressions) {
      dynamicExpressions.push(`\${__wrnexusEscapeHtml(${expr})}`);
      const sentinel = `\x00WRNEACH${dynamicExpressions.length - 1}\x00`;
      return `<span data-text="${attrEscape(expr)}">${sentinel}</span>`;
    }
    let value: unknown;
    try {
      value = new Function("with(this){return (" + expr + ");}").call(reactive.scope);
    } catch {
      return whole; // can't evaluate → keep as a client-only mustache
    }
    const baked = htmlTextEscape(value == null ? "" : String(value));
    return `<span data-text="${attrEscape(expr)}">${baked}</span>`;
  });
}

type EachNode = Extract<ViewNode, { type: "each" }>;
type IfNode = Extract<ViewNode, { type: "if" }>;

/**
 * Bake a loop-body text run into template-literal source: static text is escaped
 * for the literal, `{expr}` becomes `${__wrnexusEscapeHtml(expr)}` (server-rendered,
 * escaped), and `{t:key}` becomes a `data-t` marker resolved later by translateHtml.
 */
function bakeLoopText(raw: string): string {
  let out = "";
  let last = 0;
  let m: RegExpExecArray | null;
  const re = /\{([^{}]+)\}/g;
  while ((m = re.exec(raw))) {
    out += escLit(raw.slice(last, m.index));
    const expr = m[1]!.trim();
    if (expr.startsWith("t:")) {
      out += escLit(`<span data-t="${attrEscape(expr.slice(2).trim())}"></span>`);
    } else {
      out += "${__wrnexusEscapeHtml(" + expr + ")}";
    }
    last = m.index + m[0].length;
  }
  return out + escLit(raw.slice(last));
}

/** Bake a loop-body attribute value (same rules as text; escapeHtml is attribute-safe). */
function bakeLoopAttr(raw: string, typed = false): string {
  const wholeExpression = wholeAttributeExpression(raw);
  if (typed && wholeExpression) return "${__wrnexusPropAttr(" + wholeExpression + ")}";
  if (!raw.includes("{")) return escLit(attrEscape(raw));
  let out = "";
  let last = 0;
  let m: RegExpExecArray | null;
  const re = /\{([^{}]+)\}/g;
  while ((m = re.exec(raw))) {
    out += escLit(attrEscape(raw.slice(last, m.index)));
    out += "${__wrnexusEscapeHtml(" + m[1]!.trim() + ")}";
    last = m.index + m[0].length;
  }
  return out + escLit(attrEscape(raw.slice(last)));
}

/** Render one loop-body node to template-literal source (nested loops inline). */
function renderLoopBody(node: ViewNode): string {
  if (node.type === "text") {
    return bakeLoopText(node.value);
  }

  if (node.type === "each") {
    return compileEachExpr(node);
  }

  if (node.type === "if") {
    return compileIfExpr(node);
  }

  const componentTag = isComponentTag(node.tag);

  const attrs = node.attrs
    .filter((attr) => attr.name !== "data-component")
    .map((attr) => {
      const name = attr.event
        ? componentTag
          ? componentEventAttribute(attr.name)
          : eventAttribute(attr.name)
        : attr.name;

      if (attr.boolean) {
        return escLit(` ${name}`);
      }

      return escLit(` ${name}="`) + bakeLoopAttr(attr.value, componentTag) + escLit(`"`);
    })
    .join("");

  const inner = node.children.map(renderLoopBody).join("");

  if (node.tag === "Static") return inner;
  if (node.tag === "Dynamic")
    return (
      escLit('<wrn-dynamic-region data-wrn-dynamic="true">') +
      inner +
      escLit("</wrn-dynamic-region>")
    );
  if (node.tag === "KeepAlive") {
    const key = node.attrs.find((attribute) => attribute.name === "key")?.value ?? "default";
    return (
      escLit('<div data-wrn-keepalive="') +
      bakeLoopAttr(key) +
      escLit(`">`) +
      inner +
      escLit("</div>")
    );
  }
  if (node.tag === "Portal") {
    const target = node.attrs.find((attribute) => attribute.name === "to")?.value ?? "body";
    return (
      escLit('<div data-wrn-portal="') +
      bakeLoopAttr(target) +
      escLit('">') +
      inner +
      escLit("</div>")
    );
  }
  if (node.tag === "Transition") {
    const name =
      node.attrs.find((attribute) => attribute.name === "name")?.value ?? "wrn-transition";
    return (
      escLit('<div data-wrn-transition="') +
      bakeLoopAttr(name) +
      escLit('">') +
      inner +
      escLit("</div>")
    );
  }
  if (node.tag === "Component") {
    const selected = node.attrs.find((attribute) => attribute.name === "is")?.value ?? "";
    return (
      escLit('<div data-wrn-dynamic-component="') +
      bakeLoopAttr(selected) +
      escLit('">') +
      inner +
      escLit("</div>")
    );
  }

  if (componentTag) {
    return (
      escLit(`<div data-component="${attrEscape(node.tag)}"`) +
      attrs +
      escLit(">") +
      inner +
      escLit("</div>")
    );
  }

  if (VOID_ELEMENTS.has(node.tag.toLowerCase())) {
    return escLit(`<${node.tag}`) + attrs + escLit(">");
  }

  return escLit(`<${node.tag}`) + attrs + escLit(">") + inner + escLit(`</${node.tag}>`);
}

/**
 * Compile a `{#each list as item}` block to a `${…}` template-literal interpolation
 * that iterates the (server-evaluated) list and joins the per-item body. `list` is a
 * JS expression evaluated where `ssr` data bindings are in scope as raw named values.
 */
function compileEachExpr(node: EachNode): string {
  const item = node.item;
  const index = node.index ?? "__wi";
  const body = node.body.map(renderLoopBody).join("");
  const empty = node.empty.map(renderLoopBody).join("");
  return (
    "${(() => { const __wl = Array.isArray(" +
    node.list +
    ") ? (" +
    node.list +
    ") : []; return __wl.length ? __wl.map((" +
    item +
    ", " +
    index +
    ") => `" +
    body +
    '`).join("") : `' +
    empty +
    "`; })()}"
  );
}

/**
 * Compile a `{#if}` block to a `${…}` template-literal interpolation: a nested ternary
 * that renders the first truthy branch's body (or the `{:else}` body, or "" when neither).
 * Conditions are JS expressions evaluated in the surrounding server scope.
 */
function compileIfExpr(node: IfNode): string {
  let expr = "``"; // no matching branch → empty string
  for (let k = node.branches.length - 1; k >= 0; k--) {
    const b = node.branches[k]!;
    const bodySrc = "`" + b.body.map(renderLoopBody).join("") + "`";
    expr = b.cond === null ? bodySrc : "(" + b.cond + ") ? " + bodySrc + " : " + expr;
  }
  return "${" + expr + "}";
}

/**
 * Collect every server-control expression in a view (recursively): `{#each}` list
 * expressions and `{#if}` conditions. Used to wire up raw SSR data consts.
 */
function collectControlExprs(nodes: ViewNode[], out: string[] = []): string[] {
  for (const node of nodes) {
    if (node.type === "text") continue;
    if (node.type === "each") {
      out.push(node.list);
      collectControlExprs(node.body, out);
      collectControlExprs(node.empty, out);
    } else if (node.type === "if") {
      for (const b of node.branches) {
        if (b.cond) out.push(b.cond);
        collectControlExprs(b.body, out);
      }
    } else if (node.type === "element") {
      collectControlExprs(node.children, out);
    }
  }
  return out;
}

function renderNode(
  node: ViewNode,
  ssrBindings: SsrBinding[],
  csrBindings: CsrBinding[],
  apiBindings: Map<string, NamedDataBinding>,
  loops: string[],
  reactive: PageReactive | null = null,
): string {
  if (node.type === "text") return substituteReactiveText(node.value, reactive, loops); // {t:key} + state baking

  // Server control block (loop / conditional) → a sentinel that survives
  // templateEscape, swapped for its real `${…}` code after escaping.
  if (node.type === "each" || node.type === "if") {
    loops.push(node.type === "each" ? compileEachExpr(node) : compileIfExpr(node));
    return `\x00WRNEACH${loops.length - 1}\x00`;
  }

  if (node.tag === "Static" || node.tag === "Dynamic") {
    const inner = node.children
      .map((child) => renderNode(child, ssrBindings, csrBindings, apiBindings, loops, reactive))
      .join("");
    return node.tag === "Static"
      ? inner
      : `<wrn-dynamic-region data-wrn-dynamic="true">${inner}</wrn-dynamic-region>`;
  }
  if (node.tag === "Portal" || node.tag === "Transition" || node.tag === "Component") {
    const inner = node.children
      .map((child) => renderNode(child, ssrBindings, csrBindings, apiBindings, loops, reactive))
      .join("");
    const attribute =
      node.tag === "Portal"
        ? "data-wrn-portal"
        : node.tag === "Transition"
          ? "data-wrn-transition"
          : "data-wrn-dynamic-component";
    const source = node.tag === "Portal" ? "to" : node.tag === "Transition" ? "name" : "is";
    const fallback =
      node.tag === "Portal" ? "body" : node.tag === "Transition" ? "wrn-transition" : "";
    const original = node.attrs.find((item) => item.name === source);
    const rendered = original
      ? renderAttrs([{ ...original, name: attribute }], undefined, reactive, loops)
      : ` ${attribute}="${attrEscape(fallback)}"`;
    return `<div${rendered}>${inner}</div>`;
  }

  if (node.tag === "Async") {
    const source = attrValue(node.attrs, "source") ?? "data";
    const retries = attrValue(node.attrs, "retries") ?? "2";
    const tags = attrValue(node.attrs, "tags") ?? source;
    const serverResolved = attrValue(node.attrs, "data-wrn-async-server") === "true";
    const branchElement = (name: string) =>
      node.children.find(
        (child): child is Extract<ViewNode, { type: "element" }> =>
          child.type === "element" && child.tag === name,
      );
    const branch = (name: string) => {
      const element = branchElement(name);
      return (element?.children ?? [])
        .map((child) => renderNode(child, ssrBindings, csrBindings, apiBindings, loops, reactive))
        .join("");
    };
    const loading = branch("Loading");
    const success = branch("Success");
    const error = branch("Error");
    const successElement = branchElement("Success");
    const errorElement = branchElement("Error");
    const identifier = (value: string | undefined, fallback: string) =>
      value && isSafeGeneratedIdentifier(value) ? value : fallback;
    const successAlias = identifier(
      successElement ? attrValue(successElement.attrs, "data") : undefined,
      identifier(source, "data"),
    );
    const errorAlias = identifier(
      errorElement ? attrValue(errorElement.attrs, "error") : undefined,
      "error",
    );
    const nested = (value: string) => value.replace(/\\/g, "\\\\").replace(/`/g, "\\`");
    const scoped = (value: string, alias: string, expression: string) => {
      const index =
        loops.push(
          `\${(() => { const ${alias} = ${expression}; return \`${nested(value)}\`; })()}`,
        ) - 1;
      return `\x00WRNEACH${index}\x00`;
    };
    const successTemplate = scoped(success, successAlias, `(ctx[${JSON.stringify(source)}] ?? {})`);
    const errorTemplate = scoped(error, errorAlias, `{ message: "" }`);
    let initial = loading;
    if (serverResolved) {
      const sourcePattern = successAlias.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
      const serverSuccess = success.replace(
        new RegExp(`\\{\\s*(${sourcePattern}(?:\\.[A-Za-z_$][\\w$]*)*)\\s*\\}`, "g"),
        (_whole, expression: string) => `\${__wrnexusEscapeHtml(${expression})}`,
      );
      const index =
        loops.push(
          `\${ctx[${JSON.stringify(source)}] !== undefined ? (() => { const ${successAlias} = ctx[${JSON.stringify(source)}]; return \`${nested(serverSuccess)}\`; })() : \`${nested(loading)}\`}`,
        ) - 1;
      initial = `\x00WRNEACH${index}\x00`;
    }
    return `<section data-wrn-async="${attrEscape(source)}" data-wrn-async-tags="${attrEscape(tags)}" data-wrn-async-retries="${attrEscape(retries)}"${serverResolved ? ' data-wrn-async-resolved="true"' : ""} aria-busy="${serverResolved ? "false" : "true"}"><div data-wrn-async-content>${initial}</div><template data-wrn-async-loading>${loading}</template><template data-wrn-async-success data-wrn-async-alias="${attrEscape(successAlias)}">${successTemplate}</template><template data-wrn-async-error data-wrn-async-alias="${attrEscape(errorAlias)}">${errorTemplate}</template></section>`;
  }

  if (node.tag === "KeepAlive") {
    const key = attrValue(node.attrs, "key") ?? "default";
    const inner = node.children
      .map((child) => renderNode(child, ssrBindings, csrBindings, apiBindings, loops, reactive))
      .join("");
    return `<div data-wrn-keepalive="${attrEscape(key)}">${inner}</div>`;
  }
  if (isComponentTag(node.tag)) {
    return renderPageComponentInvocation(
      node,
      ssrBindings,
      csrBindings,
      apiBindings,
      loops,
      reactive,
    );
  }

  const apiName = attrValue(node.attrs, "api");
  const apiBinding = apiName ? apiBindings.get(apiName) : undefined;
  if (apiName && !apiBinding) {
    throw new Error(`Unknown .wrn api binding "${apiName}"`);
  }

  const ssrGet = attrValue(node.attrs, "ssrGet");
  const ssrText = attrValue(node.attrs, "ssrText");
  const csrGet = attrValue(node.attrs, "csrGet");
  const csrText = attrValue(node.attrs, "csrText");

  const csrId =
    apiBinding?.mode === "client"
      ? csrMarker(csrBindings, renderBinding(apiBinding))
      : csrGet && csrText
        ? csrMarker(csrBindings, {
            method: "GET",
            path: apiRoutePath(csrGet),
            body: expressionBody(csrText),
            helpers: "",
          })
        : undefined;

  // Void elements (<br>, <img>, …) have no closing tag and no children.
  if (VOID_ELEMENTS.has(node.tag.toLowerCase())) {
    return `<${node.tag}${renderAttrs(node.attrs, csrId, reactive, loops)}>`;
  }

  const inner =
    apiBinding?.mode === "ssr"
      ? ssrMarker(ssrBindings, renderBinding(apiBinding))
      : ssrGet && ssrText
        ? ssrMarker(ssrBindings, {
            method: "GET",
            path: apiRoutePath(ssrGet),
            body: expressionBody(ssrText),
            helpers: "",
          })
        : node.children
            .map((child) =>
              renderNode(child, ssrBindings, csrBindings, apiBindings, loops, reactive),
            )
            .join("");

  return `<${node.tag}${renderAttrs(node.attrs, csrId, reactive, loops)}>${inner}</${node.tag}>`;
}

function renderPageComponentInvocation(
  node: Extract<ViewNode, { type: "element" }>,
  ssrBindings: SsrBinding[],
  csrBindings: CsrBinding[],
  apiBindings: Map<string, NamedDataBinding>,
  loops: string[],
  reactive: PageReactive | null,
): string {
  const attrs = node.attrs
    .filter((attr) => attr.name !== "data-component")
    .map((attr) => renderPageComponentAttr(attr, loops))
    .join("");

  const inner = node.children
    .map((child) => renderNode(child, ssrBindings, csrBindings, apiBindings, loops, reactive))
    .join("");

  return `<div data-component="${attrEscape(node.tag)}"` + `${attrs}>${inner}</div>`;
}

function renderNestedComponentInvocation(
  node: Extract<ViewNode, { type: "element" }>,
  ctx: CompCtx,
): string {
  let bindIndex = 0;

  const attrs = node.attrs
    .filter((attr) => attr.name !== "data-component")
    .map((attr) => {
      const spread = /^\{\.\.\.([A-Za-z_$][\w$]*)\}$/.exec(attr.name);
      if (spread) {
        return `\${__wireSpreadAttrs(${ctx.resolveExpr(spread[1]!)})}`;
      }

      if (attr.event) {
        return (
          escLit(` ${componentEventAttribute(attr.name)}="`) +
          escLit(attrEscape(attr.value)) +
          escLit(`"`)
        );
      }

      if (attr.boolean) {
        return ` ${attr.name}`;
      }

      const wholeExpression = wholeAttributeExpression(attr.value);

      const compiledValue = wholeExpression
        ? `\${__wireProp(${ctx.resolveExpr(wholeExpression)})}`
        : compileAttrValue(attr.value, ctx);

      const rendered = ` ${attr.name}="${compiledValue}"`;

      if (
        !attr.value.includes("{") ||
        (!exprRefsState(attr.value, ctx.stateNames) && !exprRefsState(attr.value, ctx.propNames))
      ) {
        return rendered;
      }

      const marker = attrEscape(JSON.stringify([attr.name, attr.value]));

      return rendered + ` data-wrn-prop-bind-${bindIndex++}="${escLit(marker)}"`;
    })
    .join("");

  const loops = loopVarsOf(node);

  const childCtx =
    loops.length > 0
      ? {
          ...ctx,
          forwardRestAttrs: false,
          loopVars: new Set([...(ctx.loopVars ?? []), ...loops]),
        }
      : { ...ctx, forwardRestAttrs: false };

  const inner = node.children.map((child) => renderComponentNode(child, childCtx)).join("");

  return (
    `<div data-component="${attrEscape(node.tag)}"` +
    `${ctx.forwardRestAttrs ? "${__wireSpreadAttrs(__attrs)}" : ""}` +
    `${attrs}>${inner}</div>`
  );
}

function ssrMarker(bindings: SsrBinding[], binding: RenderBinding): string {
  const marker = `<!--wrnexus-ssr:${bindings.length}-->`;
  bindings.push({ marker, ...binding });
  return marker;
}

function csrMarker(bindings: CsrBinding[], binding: RenderBinding): string {
  const id = String(bindings.length);
  bindings.push({ id, ...binding });
  return id;
}

function renderBinding(binding: NamedDataBinding): RenderBinding {
  return {
    method: binding.method,
    path: binding.path,
    body: binding.body,
    helpers: binding.helpers,
  };
}

function hasClientBehavior(nodes: ViewNode[]): boolean {
  return nodes.some((node) => {
    // `{t:key}` is i18n sugar resolved server-side — not client reactivity.
    if (node.type === "text") return /\{(?!t:)[^{}]+\}/.test(node.value);
    // Server control blocks render on the server; they don't add client reactivity.
    if (node.type === "each") {
      return hasClientBehavior(node.body) || hasClientBehavior(node.empty);
    }

    if (node.type === "if") {
      return node.branches.some((branch) => hasClientBehavior(branch.body));
    }
    return (
      node.attrs.some((attr) => attr.event || attr.name === "csrGet" || attr.name === "csrText") ||
      hasClientBehavior(node.children)
    );
  });
}

function apiRoutePath(path: string): string {
  const trimmed = path.trim();
  if (!trimmed.startsWith("/")) {
    throw new Error(`.wrn API paths must start with "/": ${path}`);
  }
  if (trimmed.includes("\0") || trimmed.includes("\\") || /(^|\/)\.\.(\/|$)/.test(trimmed)) {
    throw new Error(`Unsafe .wrn API path: ${path}`);
  }
  if (trimmed === "/api" || trimmed.startsWith("/api/")) return trimmed;
  return `/api${trimmed}`;
}

function expressionBody(expr: string): string {
  return `return (${expr});`;
}

function dataBody(source: string): string {
  const trimmed = source.trim();
  if (!trimmed) return "return undefined;";
  return /\breturn\b/.test(trimmed) ? trimmed : expressionBody(trimmed);
}

function modeHelpers(ast: PageAst, mode: DataMode, sharedHelpers: string): string {
  return [
    sharedHelpers,
    ...ast.modeFunctions
      .filter((block) => block.mode === mode)
      .map((block) => block.body.trim())
      .filter(Boolean),
  ]
    .filter(Boolean)
    .join("\n\n");
}

function apiBindingMap(ast: PageAst, sharedHelpers: string): Map<string, NamedDataBinding> {
  const bindings = new Map<string, NamedDataBinding>();

  for (const block of ast.dataApis) {
    if (bindings.has(block.name)) {
      throw new Error(`Duplicate .wrn api binding "${block.name}"`);
    }
    bindings.set(block.name, {
      mode: block.mode,
      method: block.method,
      path: apiRoutePath(block.path),
      body: dataBody(block.body),
      helpers: modeHelpers(ast, block.mode, sharedHelpers),
    });
  }

  return bindings;
}

function ssrRuntimeSource(): string {
  return `const __wrnexusHtmlEscapes = { "&": "&amp;", "<": "&lt;", ">": "&gt;", "\\"": "&quot;", "'": "&#39;" };
function __wrnexusEscapeHtml(value: unknown): string {
  return String(value).replace(/[&<>"']/g, (ch) => __wrnexusHtmlEscapes[ch] ?? ch);
}

type __WrnexusContext = import("@wrnexus/core").Context & {
  __wrnexusCallApi?: (path: string, method: string) => Promise<unknown>;
  localStorage?: unknown;
};

function __wrnexusEvalData(data: unknown, body: string, helpers = "", ctx: __WrnexusContext): unknown {
  const adapters = {
    cookies: ctx.cookies,
    session: ctx.session,
    localStorage: ctx.localStorage,
  };
  return new Function("$data", "$adapters", "const cookies = $adapters.cookies;\\nconst session = $adapters.session;\\nconst localStorage = $adapters.localStorage;\\nwith ($data ?? {}) {\\n" + helpers + "\\n" + body + "\\n}")(data, adapters);
}

function __wrnexusPropAttr(
  value: unknown,
): string {
  const serialized =
    value !== null &&
    typeof value === "object"
      ? JSON.stringify(value)
      : String(value == null ? "" : value);

  return serialized.replace(
    /[&<>"]/g,
    (character) =>
      character === "&"
        ? "&amp;"
        : character === "<"
          ? "&lt;"
          : character === ">"
            ? "&gt;"
            : "&quot;",
  );
}

async function __wrnexusCallApi(path: string, method: string, ctx: __WrnexusContext): Promise<unknown> {
  if (typeof ctx.__wrnexusCallApi === "function") {
    return await ctx.__wrnexusCallApi(path, method);
  }

  const url = new URL(path, ctx.req.url);
  const res = await fetch(new Request(url, { method, headers: ctx.req.headers }));
  if (!res.ok) {
    throw new Error(".wrn data API request failed with status " + res.status);
  }

  const type = res.headers.get("content-type") || "";
  return type.includes("application/json") ? await res.json() : await res.text();
}

async function __wrnexusRenderSsrBindings(html: string, ctx: __WrnexusContext): Promise<string> {
  for (const binding of __wrnexusSsrBindings) {
    const data = await __wrnexusCallApi(binding.path, binding.method, ctx);
    const value = __wrnexusEvalData(data, binding.body, binding.helpers, ctx);
    html = html.replace(binding.marker, __wrnexusEscapeHtml(value));
  }
  return html;
}`;
}

function stableHash(value: string): string {
  let hash = 0x811c9dc5;
  for (let index = 0; index < value.length; index++) {
    hash ^= value.charCodeAt(index);
    hash = Math.imul(hash, 0x01000193);
  }
  return (hash >>> 0).toString(36);
}

function hydrationId(ast: PageAst): string {
  const shape = JSON.stringify({
    kind: ast.kind,
    name: ast.name,
    props: ast.props.map((entry) => entry.name),
    events: ast.events.map((entry) => entry.name),
    states: ast.states.map((entry) => entry.name),
    computed: ast.computed.map((entry) => entry.name),
    view: ast.view,
  });
  return `${ast.name}:${stableHash(shape)}`;
}

function localStyleId(ast: PageAst): string {
  return `wrn-${ast.kind}-${stableHash(`${ast.kind}:${ast.name}`)}`;
}

function localStyleTag(ast: PageAst, styles: string[]): string {
  if (!styles.length) return "";

  const id = localStyleId(ast);
  const css = styles.map(styleEscape).join("\n");

  return `<style data-wrnexus-style="${attrEscape(ast.name)}" data-wrnexus-style-id="${attrEscape(id)}" data-wrnexus-style-owner="${attrEscape(ast.name)}" data-wrnexus-style-kind="${attrEscape(ast.kind)}">\n${css}\n</style>`;
}

function localStyleExport(ast: PageAst, styles: string[]): string | null {
  if (!styles.length) return null;

  return `export const __wrnexusStyles = ${JSON.stringify(
    [
      {
        id: localStyleId(ast),
        owner: ast.name,
        kind: ast.kind,
        css: styles.join("\n"),
      },
    ],
    null,
    2,
  )};`;
}

function isStoreImportSource(source: string): boolean {
  return /(?:^|\/)stores?\//.test(source) || source.startsWith("@wrnexus/store");
}

function importedStoreBindings(ast: PageAst): Array<{ local: string; internal: string }> {
  return ast.structuredImports
    .filter(
      (entry) =>
        entry.defaultImport && entry.source.endsWith(".wrn") && isStoreImportSource(entry.source),
    )
    .map((entry) => ({
      local: entry.defaultImport!,
      internal: `__wrnexusStoreDefinition_${entry.defaultImport}`,
    }));
}

function generatedImports(ast: PageAst): string[] {
  const stores = new Map(importedStoreBindings(ast).map((entry) => [entry.local, entry.internal]));
  return ast.structuredImports.map((entry) => {
    if (!entry.defaultImport) return entry.raw;
    const internal = stores.get(entry.defaultImport);
    return internal
      ? entry.raw.replace(
          new RegExp(
            `^(\\s*import\\s+(?:type\\s+)?)(?:${entry.defaultImport.replace(/[.*+?^${}()|[\\]\\]/g, "\\$&")})(\\s+from\\s+)`,
          ),
          `$1${internal}$2`,
        )
      : entry.raw;
  });
}

function isSafeGeneratedIdentifier(name: string): boolean {
  return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name);
}

function generateSsrStateAliases(stateNames: string[]): string {
  const names = [...new Set(stateNames)].filter(isSafeGeneratedIdentifier);

  if (!names.length) {
    return "";
  }

  return `const { ${names.join(", ")} } = __state;\n`;
}

function orderPageStates(entries: PageAst["states"]): PageAst["states"] {
  if (entries.length < 2) return entries;
  const byName = new Map(entries.map((entry) => [entry.name, entry]));
  const visiting = new Set<string>();
  const visited = new Set<string>();
  const ordered: PageAst["states"] = [];

  const visit = (name: string): void => {
    if (visited.has(name)) return;
    if (visiting.has(name)) {
      throw new Error(`WRN-STATE-CYCLE: state value '${name}' has a dependency cycle.`);
    }
    const entry = byName.get(name);
    if (!entry) return;
    visiting.add(name);
    for (const dependency of byName.keys()) {
      if (
        dependency !== name &&
        new RegExp(`\\b${dependency.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`).test(entry.expr)
      ) {
        visit(dependency);
      }
    }
    visiting.delete(name);
    visited.add(name);
    ordered.push(entry);
  };

  for (const entry of entries) visit(entry.name);
  return ordered;
}

function generateSsrStateInitializer(entries: PageAst["states"]): string {
  const ordered = orderPageStates(entries);
  const declarations = ordered
    .filter((entry) => isSafeGeneratedIdentifier(entry.name))
    .map(
      (entry) =>
        `const ${entry.name} = (() => { try { return (${entry.expr}); } catch { return undefined; } })();`,
    )
    .join(" ");
  const values = entries
    .map((entry) => {
      if (isSafeGeneratedIdentifier(entry.name)) {
        return `${JSON.stringify(entry.name)}: ${entry.name}`;
      }
      return `${JSON.stringify(entry.name)}: (() => { try { return (${entry.expr}); } catch { return undefined; } })()`;
    })
    .join(", ");
  return `(() => { ${declarations} return { ${values} }; })()`;
}

function orderPageComputed(entries: PageAst["computed"]): PageAst["computed"] {
  if (entries.length < 2) return entries;
  const byName = new Map(entries.map((entry) => [entry.name, entry]));
  const visiting = new Set<string>();
  const visited = new Set<string>();
  const ordered: PageAst["computed"] = [];

  const visit = (name: string): void => {
    if (visited.has(name)) return;
    if (visiting.has(name)) {
      throw new Error(`WRN-COMPUTED-CYCLE: computed value '${name}' has a dependency cycle.`);
    }
    const entry = byName.get(name);
    if (!entry) return;
    visiting.add(name);
    for (const dependency of byName.keys()) {
      if (
        dependency !== name &&
        new RegExp(`\\b${dependency.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`).test(entry.expr)
      ) {
        visit(dependency);
      }
    }
    visiting.delete(name);
    visited.add(name);
    ordered.push(entry);
  };

  for (const entry of entries) visit(entry.name);
  return ordered;
}

function generateSsrComputedAliases(entries: PageAst["computed"]): string {
  return orderPageComputed(entries)
    .filter((entry) => isSafeGeneratedIdentifier(entry.name))
    .map(
      (entry) =>
        `const ${entry.name} = (() => { try { return (${entry.expr}); } catch { return undefined; } })();`,
    )
    .join("\n");
}

function runtimeComputedNames(
  states: PageAst["states"],
  computed: PageAst["computed"],
  runtimeRoots: Iterable<string> = [],
): Set<string> {
  const entries = [...states, ...computed];
  const runtime = new Set([
    ...runtimeRoots,
    ...entries.filter((entry) => /\bctx\b/.test(entry.expr)).map((entry) => entry.name),
  ]);
  let changed = true;
  while (changed) {
    changed = false;
    for (const entry of entries) {
      if (runtime.has(entry.name)) continue;
      if (
        [...runtime].some((name) =>
          new RegExp(`\\b${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`).test(entry.expr),
        )
      ) {
        runtime.add(entry.name);
        changed = true;
      }
    }
  }
  return runtime;
}

function hydrationAttribute(ast: PageAst): string {
  const strategy = ["static", "server"].includes(ast.renderMode ?? "")
    ? "none"
    : (ast.hydrate ?? "load");
  const hasBrowserModule = browserModuleRequired(ast);
  const moduleAttribute = hasBrowserModule
    ? ' data-wrn-client-module="__WRNEXUS_CLIENT_MODULE__"'
    : "";
  return ` data-wrn-hydration="${attrEscape(hydrationId(ast))}" data-wrn-hydrate="${attrEscape(strategy)}" data-wrn-runtime="${attrEscape(ast.runtime ?? "universal")}"${moduleAttribute}`;
}

function targetFunctions(ast: PageAst, target: "browser" | "server"): string {
  const runtimes =
    target === "browser"
      ? (["legacy", "client", "shared"] as const)
      : (["legacy", "server", "shared"] as const);
  return ast.functions
    .map((body) => stripRuntimeFunctionModifiers(body, [...runtimes]))
    .map((body) => body.trim())
    .filter(Boolean)
    .join("\n\n");
}

function publicOutputNames(ast: PageAst): string[] {
  return [
    ...new Set([
      ...ast.outputs.map((output) => output.name),
      ...ast.events.map((event) => event.name),
    ]),
  ];
}

function prepareActionForms(nodes: ViewNode[], actions: ReadonlySet<string>): void {
  for (const node of nodes) {
    if (node.type === "text") continue;
    if (node.type === "each") {
      prepareActionForms(node.body, actions);
      prepareActionForms(node.empty, actions);
      continue;
    }
    if (node.type === "if") {
      node.branches.forEach((branch) => prepareActionForms(branch.body, actions));
      continue;
    }
    prepareActionForms(node.children, actions);
    if (node.tag.toLowerCase() !== "form") continue;
    const submit = node.attrs.find((attr) => attr.event && attr.name === "submit");
    if (!submit || !actions.has(submit.value.trim())) continue;
    const name = submit.value.trim();
    node.attrs = node.attrs.filter((attr) => attr !== submit);
    if (!node.attrs.some((attr) => !attr.event && attr.name === "method")) {
      node.attrs.push({ name: "method", value: "post", event: false });
    }
    node.attrs.push({ name: "data-wrn-action", value: name, event: false });
    node.children.unshift({
      type: "element",
      tag: "input",
      attrs: [
        { name: "type", value: "hidden", event: false },
        { name: "name", value: "_wrnexus_action", event: false },
        { name: "value", value: name, event: false },
      ],
      children: [],
    });
  }
}

function markServerAsyncBoundaries(nodes: ViewNode[], serverLoads: ReadonlySet<string>): void {
  for (const node of nodes) {
    if (node.type === "text") continue;
    if (node.type === "each") {
      markServerAsyncBoundaries(node.body, serverLoads);
      markServerAsyncBoundaries(node.empty, serverLoads);
      continue;
    }
    if (node.type === "if") {
      node.branches.forEach((branch) => markServerAsyncBoundaries(branch.body, serverLoads));
      continue;
    }
    if (node.tag === "Async") {
      const source = attrValue(node.attrs, "source") ?? "data";
      if (
        serverLoads.has(source) &&
        !node.attrs.some((attribute) => attribute.name === "data-wrn-async-server")
      ) {
        node.attrs.push({ name: "data-wrn-async-server", value: "true", event: false });
      }
    }
    markServerAsyncBoundaries(node.children, serverLoads);
  }
}

export function generate(ast: PageAst): string {
  ast = optimizeAst(ast).ast;
  if (ast.kind === "global-store" || ast.kind === "page-store") return generateStoreModule(ast);
  if (ast.kind === "component" || ast.kind === "layout") {
    return generateComponent(ast);
  }

  const out: string[] = [];
  prepareActionForms(ast.view, new Set(ast.actions.map((action) => action.name)));
  markServerAsyncBoundaries(
    ast.view,
    new Set(
      ast.loads
        .filter((load) => load.mode === "server" && !load.deferred && load.name)
        .map((load) => load.name!),
    ),
  );
  if (ast.actions.length > 0) {
    out.push(
      `import { createActionClient } from "@wrnexus/csr";\nimport type { InferSchema } from "@wrnexus/validation";`,
    );
  }
  if (ast.imports.length > 0) out.push(generatedImports(ast).join("\n"));
  const ssrBindings: SsrBinding[] = [];
  const csrBindings: CsrBinding[] = [];
  const helpers = targetFunctions(ast, "server");
  const apiBindings = apiBindingMap(ast, helpers);

  const typeSource = ast.types
    .map((body) => body.trim())
    .filter(Boolean)
    .join("\n\n");
  if (typeSource) out.push(typeSource);

  if (helpers) {
    out.push(`// --- .wrn functions ---\n${helpers}`);
  }

  // --- Page metadata / SEO ---
  out.push(`export const meta = ${JSON.stringify({ title: ast.name, ...ast.seo }, null, 2)};`);
  if (ast.layout)
    out.push(
      `export const layout = ${ast.layoutIsSymbol ? ast.layout : JSON.stringify(ast.layout)};`,
    );
  out.push(`export const __wrnexusRuntime = ${JSON.stringify(ast.runtime ?? "universal")};`);
  out.push(`export const __wrnexusRender = ${JSON.stringify(ast.renderMode ?? "hybrid")};`);
  out.push(
    `export const __wrnexusHydrate = ${JSON.stringify(["static", "server"].includes(ast.renderMode ?? "") ? "none" : (ast.hydrate ?? "load"))};`,
  );
  out.push(`export const __wrnexusHydrationId = ${JSON.stringify(hydrationId(ast))};`);
  if (Object.keys(ast.cache ?? {}).length > 0)
    out.push(`export const __wrnexusCache = ${JSON.stringify(ast.cache, null, 2)};`);
  if (Object.keys(ast.security).length > 0) {
    out.push(`export const __wrnexusSecurity = ${JSON.stringify(ast.security, null, 2)};`);
  }
  if (Object.keys(ast.navigation).length > 0) {
    out.push(`export const __wrnexusNavigation = ${JSON.stringify(ast.navigation, null, 2)};`);
  }

  // --- View -> default page component ---
  const orderedStates = orderPageStates(ast.states);
  const browserStates = ast.states.filter((state) => state.runtime !== "server");
  const seedScope = evalStateSeeds(orderedStates);
  const orderedComputed = orderPageComputed(ast.computed);
  for (const entry of orderedComputed) {
    try {
      seedScope[entry.name] = new Function("with(this){return (" + entry.expr + ");}").call(
        seedScope,
      );
    } catch {
      seedScope[entry.name] = undefined;
    }
  }
  const reactiveNames = [
    ...browserStates.map((entry) => entry.name),
    ...ast.computed.map((entry) => entry.name),
  ];
  const storeBindings = importedStoreBindings(ast);
  const runtimeRoots = [
    ...storeBindings.map((entry) => entry.local),
    ...ast.loads
      .filter((load) => load.mode === "server" && !load.deferred && load.name)
      .map((load) => load.name!),
  ];
  const runtimeStateNames = runtimeComputedNames(orderedStates, orderedComputed, runtimeRoots);
  const reactive: PageReactive | null =
    reactiveNames.length > 0
      ? { stateNames: new Set(reactiveNames), runtimeStateNames, scope: seedScope }
      : null;
  const loops: string[] = [];
  let html = ast.view
    .map((node) => renderNode(node, ssrBindings, csrBindings, apiBindings, loops, reactive))
    .join("");
  const styles = ast.styles.map((body) => body.trim()).filter(Boolean);
  const pageBehavior = ast.runtime === "server" ? null : componentBehavior(ast);
  const needsClientRuntime =
    ast.runtime !== "server" &&
    (browserStates.length > 0 ||
      ast.computed.length > 0 ||
      hasClientBehavior(ast.view) ||
      pageBehavior !== null);

  if (needsClientRuntime) {
    const scopePlaceholder = "__WRNEXUS_DYNAMIC_SCOPE__";
    html = `<div data-scope="${scopePlaceholder}"${behaviorAttribute(pageBehavior)}${hydrationAttribute(ast)}>${html}</div>`;
  }

  const pageStyleTag = localStyleTag(ast, styles);
  if (pageStyleTag) {
    html = `${pageStyleTag}${html}`;
  }
  if (ast.renderMode === "client") {
    const clientRoot = hydrationId(ast);
    html = `<div data-wrn-client-root="${clientRoot}" aria-busy="true"></div><template data-wrn-client-template="${clientRoot}">${html}</template>`;
  }
  const pageStyleExport = localStyleExport(ast, styles);
  if (pageStyleExport) out.push(pageStyleExport);
  if (csrBindings.length > 0) {
    out.push(`export const __wrnexusCsr = ${JSON.stringify(csrBindings, null, 2)};`);
  }
  if (pageBehavior) {
    out.push(`export const __wrnexusBehavior = ${JSON.stringify(pageBehavior, null, 2)};`);
  }

  // Escape the static HTML for the template literal, then swap loop sentinels for
  // their real `${…}` code (which must NOT be escaped).
  let body = templateEscape(html);
  let staticShellBody: string | undefined;
  if (ast.renderMode === "partial-static") {
    const shellHtml = html.replace(
      /<wrn-dynamic-region\b[^>]*>[\s\S]*?<\/wrn-dynamic-region>/gi,
      '<wrn-dynamic-region data-wrn-dynamic="true"></wrn-dynamic-region>',
    );
    staticShellBody = templateEscape(shellHtml);
  }
  const dynamicStateInitializer = generateSsrStateInitializer(orderedStates);
  const stateType =
    ast.states.length > 0
      ? `{ ${ast.states
          .map((state) => `${JSON.stringify(state.name)}: ${state.valueType ?? "unknown"}`)
          .join("; ")} }`
      : "Record<string, never>";
  const hydrationStateNames = JSON.stringify(browserStates.map((state) => state.name));

  const ssrStateAliases = generateSsrStateAliases(ast.states.map((state) => state.name));
  const ssrComputedAliases = generateSsrComputedAliases(orderedComputed);

  const storeDeclarations = storeBindings
    .map(
      (entry) => `        const ${entry.local} = await ctx.__wrnexusUseStore(${entry.internal});`,
    )
    .join("\n");
  const serverLoadAliases = ast.loads
    .filter((load) => load.mode === "server" && !load.deferred && load.name)
    .map((load) => `        const ${load.name} = ctx[${JSON.stringify(load.name)}];`)
    .join("\n");

  // Inner dynamic expressions are registered before the wrapper that scopes
  // them (for example an Async branch alias). Resolve from the outside in so a
  // wrapper sentinel is expanded before its nested sentinels are visited.
  for (let idx = loops.length - 1; idx >= 0; idx--) {
    const code = loops[idx]!;
    body = body.replaceAll(`\x00WRNEACH${idx}\x00`, code);
    if (staticShellBody?.includes(`\x00WRNEACH${idx}\x00`)) {
      staticShellBody = staticShellBody.replaceAll(`\x00WRNEACH${idx}\x00`, code);
    }
  }

  // Server loops iterate raw SSR data. Declare a named const for every `ssr` data
  // binding a loop references, so `{#each <name> as …}` can iterate the real value.
  const loopConsts: string[] = [];
  if (loops.length > 0) {
    const lists = collectControlExprs(ast.view);
    for (const [name, binding] of apiBindings) {
      if (binding.mode !== "ssr") continue;
      if (!lists.some((expr) => new RegExp(`\\b${name}\\b`).test(expr))) continue;
      loopConsts.push(
        `  const ${name} = __wrnexusEvalData(await __wrnexusCallApi(${JSON.stringify(binding.path)}, ${JSON.stringify(binding.method)}, ctx), ${JSON.stringify(binding.body)}, ${JSON.stringify(binding.helpers)}, ctx);`,
      );
    }
  }

  const needsSsrRuntime = ssrBindings.length > 0 || loops.length > 0 || runtimeStateNames.size > 0;
  if (needsSsrRuntime) {
    out.push(ssrRuntimeSource());
    out.push(`const __wrnexusSsrBindings = ${JSON.stringify(ssrBindings, null, 2)};`);
    const decls = loopConsts.length > 0 ? loopConsts.join("\n") + "\n" : "";
    out.push(
      `export default async function ${ast.name}(ctx: __WrnexusContext) {
        ${storeDeclarations}
        ${serverLoadAliases}
      ${decls}
        const __state: ${stateType} = ${dynamicStateInitializer};
        ${ssrStateAliases}
        ${ssrComputedAliases}
        const __hydrationState = Object.fromEntries(${hydrationStateNames}.map((key) => [key, Reflect.get(__state, key)]));
        const __scopeValue = Object.entries(__hydrationState)
          .map(([key, value]) => {
            let encoded: string;
            try {
              encoded = value === undefined ? "undefined" : JSON.stringify(value);
            } catch {
              encoded = JSON.stringify(String(value));
            }
            return key + ": " + encoded;
          })
          .join(", ")
          .replace(/&/g, "&amp;")
          .replace(/"/g, "&quot;")
          .replace(/</g, "&lt;")
          .replace(/>/g, "&gt;");

        const html = \`${body}\`.replace(
          "__WRNEXUS_DYNAMIC_SCOPE__",
          __scopeValue,
        );

        return await __wrnexusRenderSsrBindings(html, ctx);
      }`,
    );
  } else {
    out.push(
      `export default ${storeBindings.length > 0 ? "async " : ""}function ${ast.name}(ctx: import("@wrnexus/core").Context) {
        ${storeDeclarations}
        ${serverLoadAliases}
        const __state: ${stateType} = ${dynamicStateInitializer};
        ${ssrStateAliases}
        ${ssrComputedAliases}
        const __hydrationState = Object.fromEntries(${hydrationStateNames}.map((key) => [key, Reflect.get(__state, key)]));
        const __scopeValue = Object.entries(__hydrationState)
          .map(([key, value]) => {
            let encoded: string;
            try {
              encoded = value === undefined ? "undefined" : JSON.stringify(value);
            } catch {
              encoded = JSON.stringify(String(value));
            }
            return key + ": " + encoded;
          })
          .join(", ")
          .replace(/&/g, "&amp;")
          .replace(/"/g, "&quot;")
          .replace(/</g, "&lt;")
          .replace(/>/g, "&gt;");

        return \`${body}\`.replace(
          "__WRNEXUS_DYNAMIC_SCOPE__",
          __scopeValue,
        );
      }`,
    );
  }

  if (staticShellBody !== undefined) {
    out.push(
      `export async function __wrnexusBuildStaticShell(ctx: import("@wrnexus/core").Context = {} as import("@wrnexus/core").Context) {
        ${storeDeclarations}
        ${serverLoadAliases}
        ${loopConsts.length > 0 ? loopConsts.join("\n") : ""}
        const __state: ${stateType} = ${dynamicStateInitializer};
        ${ssrStateAliases}
        ${ssrComputedAliases}
        const __hydrationState = Object.fromEntries(${hydrationStateNames}.map((key) => [key, Reflect.get(__state, key)]));
        const __scopeValue = Object.entries(__hydrationState)
          .map(([key, value]) => {
            let encoded: string;
            try {
              encoded = value === undefined ? "undefined" : JSON.stringify(value);
            } catch {
              encoded = JSON.stringify(String(value));
            }
            return key + ": " + encoded;
          })
          .join(", ")
          .replace(/&/g, "&amp;")
          .replace(/"/g, "&quot;")
          .replace(/</g, "&lt;")
          .replace(/>/g, "&gt;");
        return \`${staticShellBody}\`.replace("__WRNEXUS_DYNAMIC_SCOPE__", __scopeValue);
      }`,
    );
  }

  if (ast.loads.length > 0) {
    const serverLoads = ast.loads.filter((entry) => entry.mode === "server" && !entry.deferred);
    const publicClientLoads = ast.loads.filter(
      (entry) => entry.mode === "client" || entry.deferred,
    );
    const namedByName = new Map(
      ast.loads.filter((entry) => entry.name).map((entry) => [entry.name!, entry]),
    );
    const clientNames = new Set(
      publicClientLoads.flatMap((entry) => (entry.name ? [entry.name] : [])),
    );
    const includeDependencies = (name: string): void => {
      for (const dependency of namedByName.get(name)?.dependsOn ?? []) {
        if (clientNames.has(dependency)) continue;
        clientNames.add(dependency);
        includeDependencies(dependency);
      }
    };
    for (const name of [...clientNames]) includeDependencies(name);
    const clientLoads = ast.loads.filter((entry) => !entry.name || clientNames.has(entry.name));
    const renderLoads = (
      exportName: string,
      execution: typeof ast.loads,
      exposed: typeof ast.loads,
    ): string => {
      const declarations = execution
        .filter((entry) => entry.name)
        .map((entry) => {
          const dependencies = (entry.dependsOn ?? [])
            .map((dependency) => `const ${dependency} = await __load_${dependency}();`)
            .join("\n");
          return `  let __promise_${entry.name}: Promise<unknown> | undefined;
  const __load_${entry.name} = () => (__promise_${entry.name} ??= (async () => {
    ${dependencies}
    ${entry.body}
  })());`;
        })
        .join("\n");
      const visible = exposed.filter((entry) => entry.name);
      return `export async function ${exportName}(ctx: import("@wrnexus/core").Context) {
${exposed
  .filter((entry) => !entry.name)
  .map((entry) => entry.body)
  .join("\n")}
${declarations}
${
  visible.length
    ? `  const __values = await Promise.all([${visible.map((entry) => `__load_${entry.name}()`).join(", ")}]);
  return { ${visible.map((entry, index) => `${JSON.stringify(entry.name)}: __values[${index}]`).join(", ")} };`
    : ""
}
}`;
    };
    if (serverLoads.length > 0) out.push(renderLoads("__wrnexusLoad", serverLoads, serverLoads));
    if (publicClientLoads.length > 0)
      out.push(renderLoads("__wrnexusClientLoad", clientLoads, publicClientLoads));
  }

  if (ast.actions.length > 0) {
    for (const action of ast.actions) {
      if (!action.schema) {
        out.push(
          `export async function ${action.name}(${action.args.join(", ")}) {${action.body}}`,
        );
        continue;
      }
      out.push(`export async function ${action.name}(input: InferSchema<typeof ${action.schema}>, ctx: import("@wrnexus/core").Context) {
  const invalidate = (...tags: string[]) => {
    const bucket = (ctx.locals.__wrnexusInvalidatedTags ??= []);
    bucket.push(...tags.flat());
  };
${action.body}
}`);
    }
    out.push(
      `export const __wrnexusActions = { ${ast.actions
        .map(
          (action) =>
            `${action.name}: { run: ${action.name}, schema: ${action.schema ?? "undefined"} }`,
        )
        .join(", ")} };`,
    );
    out.push(`export const __wrnexusActionClients = {
${ast.actions
  .map(
    (action) =>
      `  ${action.name}: createActionClient<${action.schema ? `InferSchema<typeof ${action.schema}>` : "Record<string, unknown>"}, Awaited<ReturnType<typeof ${action.name}>>>("", ${JSON.stringify(action.name)}),`,
  )
  .join("\n")}
};`);
  }

  // --- API blocks -> method handlers ---
  if (ast.apis.length > 0) {
    ast.apis.forEach((api, index) => {
      const name = `__wrnexusApi_${api.method}_${index}`;
      out.push(`// ${api.method} ${apiRoutePath(api.path)}
const ${name} = async (ctx: import("@wrnexus/core").Context) => {${api.body}};`);
    });

    const entries = ast.apis.map(
      (api, index) =>
        `  ${JSON.stringify(`${api.method} ${apiRoutePath(api.path)}`)}: __wrnexusApi_${api.method}_${index},`,
    );
    out.push(`export const __wrnexusApi = {\n${entries.join("\n")}\n};`);

    const exported = new Set<string>();
    ast.apis.forEach((api, index) => {
      if (exported.has(api.method)) return;
      exported.add(api.method);
      out.push(`export const ${api.method} = __wrnexusApi_${api.method}_${index};`);
    });
  }

  // --- Realtime blocks -> a websocket export ---
  if (ast.realtimes.length > 0) {
    const handlers = ast.realtimes.flatMap((rt) =>
      rt.handlers.map((h) => {
        const params = ["ws", ...h.args].join(", ");
        return `  ${h.event}(${params}: Event) {${h.body}},`;
      }),
    );
    out.push(`export const websocket = {\n${handlers.join("\n")}\n};`);
  }

  return out.join("\n\n") + "\n";
}

/**
 * Lower a `component` AST to a module exporting `render(props)`.
 *
 * A component is server-rendered on demand at each `data-component` mount and
 * hydrated on the browser by the generic reactive runtime — it ships no JS of
 * its own. Declared props are coerced to the type of their default value, then
 * seeded (with any `state`) into the `data-scope` the reactive runtime reads.
 */
interface CompCtx {
  /** State names — text referencing any of them stays a reactive client mustache. */
  stateNames: Set<string>;
  /** Props are signals too, allowing a parent to drive a mounted child. */
  propNames: Set<string>;
  /** Component functions can read state and therefore make their callers reactive. */
  functionNames: Set<string>;
  /** Rewrite reserved-word prop/state identifiers to their safe const names. */
  resolveExpr: (expr: string) => string;
  /** `data-for` loop variables in scope — their mustaches stay literal for the
   * client's list renderer (never baked server-side, since they have no value). */
  loopVars?: Set<string>;
  /** Local identifiers introduced by server-rendered `{#each}` blocks. */
  serverLocals?: Set<string>;
  /** Forward undeclared component attributes to this element only. */
  forwardRestAttrs?: boolean;
  /** Public component events exposed from the component root. */
  eventNames?: string[];
}

interface ComponentBehavior {
  functions: string;
  outputs: Array<{
    name: string;
    payload?: { name: string; valueType: string; optional: boolean };
  }>;
  computed: Array<{ name: string; expr: string }>;
  effects: string[];
  lifecycle: {
    mount?: string;
    update?: string;
    unmount?: string;
  };
  watches: Array<{
    state: string;
    body: string;
  }>;
}

/** Parse a `data-for="item in list [key expr]"` / `"item, i in list [key expr]"` directive. */
export function parseForExpr(
  value: string,
): { item: string; index?: string; list: string; key?: string } | null {
  const m =
    /^\s*([A-Za-z_$][\w$]*)\s*(?:,\s*([A-Za-z_$][\w$]*)\s*)?\s+in\s+([\s\S]+?)(?:\s+key\s+([\s\S]+?))?\s*$/.exec(
      value,
    );
  if (!m) return null;
  return { item: m[1]!, index: m[2], list: m[3]!.trim(), key: m[4]?.trim() };
}

/** The loop variables a node introduces via `data-for`, if any. */
function loopVarsOf(node: ViewNode): string[] {
  if (node.type !== "element") return [];
  const attr = node.attrs.find((a) => !a.event && a.name === "data-for");
  if (!attr) return [];
  const parsed = parseForExpr(attr.value);
  return parsed ? [parsed.item, ...(parsed.index ? [parsed.index] : [])] : [];
}

/** JS reserved words that cannot be used as a plain `const` name. */
const JS_RESERVED = new Set([
  "class",
  "for",
  "default",
  "function",
  "return",
  "if",
  "else",
  "new",
  "delete",
  "typeof",
  "in",
  "instanceof",
  "void",
  "do",
  "while",
  "switch",
  "case",
  "break",
  "continue",
  "this",
  "super",
  "import",
  "export",
  "extends",
  "var",
  "let",
  "const",
  "null",
  "true",
  "false",
  "try",
  "catch",
  "finally",
  "throw",
  "yield",
  "await",
  "enum",
  "with",
  "debugger",
  "implements",
  "interface",
  "package",
  "private",
  "protected",
  "public",
  "static",
]);

/** A JS reference for a prop/state name (reserved words get a `__p_` prefix). */
function safeRef(name: string): string {
  return JS_RESERVED.has(name) ? `__p_${name}` : name;
}

/** Escape a literal segment so it is safe inside a JS template literal. */
function escLit(s: string): string {
  return s.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$\{/g, "\\${");
}

function componentBehavior(ast: PageAst): ComponentBehavior | null {
  const functions = eraseFunctionTypes(targetFunctions(ast, "browser"));

  const computed = ast.computed.map((entry) => ({ name: entry.name, expr: entry.expr.trim() }));
  const effects = ast.effects.map((entry) => entry.body.trim()).filter(Boolean);

  const lifecycle = {
    ...(ast.lifecycle.mount?.trim() ? { mount: ast.lifecycle.mount.trim() } : {}),

    ...(ast.lifecycle.update?.trim() ? { update: ast.lifecycle.update.trim() } : {}),

    ...(ast.lifecycle.unmount?.trim() ? { unmount: ast.lifecycle.unmount.trim() } : {}),
  };

  const watches = ast.watches.map((watch) => ({
    state: watch.state,
    body: watch.body.trim(),
  }));

  if (
    !functions &&
    ast.outputs.length === 0 &&
    computed.length === 0 &&
    effects.length === 0 &&
    Object.keys(lifecycle).length === 0 &&
    watches.length === 0
  ) {
    return null;
  }

  return {
    functions,
    outputs: ast.outputs,
    computed,
    effects,
    lifecycle,
    watches,
  };
}

function behaviorAttribute(behavior: ComponentBehavior | null): string {
  if (!behavior) {
    return "";
  }

  const encoded = Buffer.from(JSON.stringify(behavior), "utf8").toString("base64");

  return ` data-wrn-behavior="${encoded}"`;
}

const INTERP_RE = /\{([^{}]+)\}/g;

function exprRefsState(expr: string, stateNames: Set<string>): boolean {
  for (const name of stateNames) {
    if (new RegExp(`\\b${name}\\b`).test(expr)) return true;
  }
  return false;
}

function exprRefsComponentReactiveValue(expr: string, ctx: CompCtx): boolean {
  return (
    exprRefsState(expr, ctx.stateNames) ||
    exprRefsState(expr, ctx.propNames) ||
    exprRefsState(expr, ctx.functionNames)
  );
}

function viewHasEvents(nodes: ViewNode[]): boolean {
  return nodes.some((node) => {
    if (node.type === "text") return false;

    if (node.type === "each") {
      return viewHasEvents(node.body) || viewHasEvents(node.empty);
    }

    if (node.type === "if") {
      return node.branches.some((branch) => viewHasEvents(branch.body));
    }

    return node.attrs.some((attr) => attr.event) || viewHasEvents(node.children);
  });
}

function viewHasServerEach(nodes: ViewNode[]): boolean {
  return nodes.some((node) => {
    if (node.type === "text") return false;

    if (node.type === "each") return true;

    if (node.type === "if") {
      return node.branches.some((branch) => viewHasServerEach(branch.body));
    }

    return viewHasServerEach(node.children);
  });
}

function viewHasRestAttributeSpread(nodes: ViewNode[]): boolean {
  return nodes.some((node) => {
    if (node.type === "text") return false;
    if (node.type === "each") {
      return viewHasRestAttributeSpread(node.body) || viewHasRestAttributeSpread(node.empty);
    }
    if (node.type === "if") {
      return node.branches.some((branch) => viewHasRestAttributeSpread(branch.body));
    }
    return (
      node.attrs.some((attr) => /^\{\.\.\.([A-Za-z_$][\w$]*)\}$/.test(attr.name)) ||
      viewHasRestAttributeSpread(node.children)
    );
  });
}

/**
 * Compile a text node. Interpolations that reference state stay as client
 * mustaches (`{expr}`, hydrated by the reactive runtime); interpolations of
 * props/constants are baked server-side (`${__wireHtml(expr)}`), so static
 * components render correct HTML with zero JavaScript.
 */
function compileText(raw: string, ctx: CompCtx): string {
  let out = "";
  let last = 0;
  let m: RegExpExecArray | null;
  INTERP_RE.lastIndex = 0;
  while ((m = INTERP_RE.exec(raw))) {
    out += escLit(raw.slice(last, m.index));
    const expr = m[1]!.trim();
    if (expr.startsWith("t:")) {
      // i18n sugar: {t:key} → a marker resolved server-side by translateHtml.
      out += escLit(`<span data-t="${attrEscape(expr.slice(2).trim())}"></span>`);
    } else if (ctx.loopVars && exprRefsState(expr, ctx.loopVars)) {
      // Loop variable (from data-for): leave a literal client mustache — the
      // list renderer fills it per item; it has no server-side value.
      out += escLit(`{${expr}}`);
    } else if (expr === "content") {
      out += `\${__wireRaw(${ctx.resolveExpr(expr)})}`;
    } else if (exprRefsComponentReactiveValue(expr, ctx)) {
      // State interpolation: bake the initial value AND keep it reactive via a
      // data-text span, so no-JS clients see the real value and hydration
      // updates it in place. `count` → `<span data-text="count">0</span>`.
      out +=
        escLit(`<span data-text="${attrEscape(expr)}">`) +
        `\${__wireHtml(${ctx.resolveExpr(expr)})}` +
        escLit(`</span>`);
    } else {
      out += `\${__wireHtml(${ctx.resolveExpr(expr)})}`;
    }
    last = m.index + m[0].length;
  }
  return out + escLit(raw.slice(last));
}

/** Compile an attribute value; `{expr}` is baked server-side (loop vars stay literal). */
function compileAttrValue(raw: string, ctx: CompCtx): string {
  if (!raw.includes("{")) return escLit(attrEscape(raw));
  let out = "";
  let last = 0;
  let m: RegExpExecArray | null;
  INTERP_RE.lastIndex = 0;
  while ((m = INTERP_RE.exec(raw))) {
    out += escLit(attrEscape(raw.slice(last, m.index)));
    const expr = m[1]!.trim();
    if (ctx.loopVars && exprRefsState(expr, ctx.loopVars)) {
      out += escLit(`{${expr}}`); // hydrated per-item by the list renderer
    } else {
      out += `\${__wireAttr(${ctx.resolveExpr(expr)})}`;
    }
    last = m.index + m[0].length;
  }
  return out + escLit(attrEscape(raw.slice(last)));
}

function renderComponentIfNode(node: IfNode, ctx: CompCtx): string {
  let expression = "``";

  for (let index = node.branches.length - 1; index >= 0; index--) {
    const branch = node.branches[index]!;
    const body = branch.body.map((child) => renderComponentNode(child, ctx)).join("");
    const bodyExpression = "`" + body + "`";

    expression =
      branch.cond === null
        ? bodyExpression
        : `(${ctx.resolveExpr(branch.cond)}) ? ${bodyExpression} : ${expression}`;
  }

  return "${" + expression + "}";
}

function renderComponentEachNode(node: EachNode, ctx: CompCtx): string {
  const item = node.item;
  const index = node.index ?? "__wi";
  const list = ctx.resolveExpr(node.list);

  const childCtx: CompCtx = {
    ...ctx,
    serverLocals: new Set([...(ctx.serverLocals ?? []), item, index]),
  };

  const body = node.body.map((child) => renderComponentNode(child, childCtx)).join("");
  const empty = node.empty.map((child) => renderComponentNode(child, ctx)).join("");

  return (
    "${(() => { const __wl = Array.isArray(" +
    list +
    ") ? (" +
    list +
    ") : []; return __wl.length ? __wl.map((" +
    item +
    ", " +
    index +
    ") => `" +
    body +
    '`).join("") : `' +
    empty +
    "`; })()}"
  );
}

function serverLoopLocalsAttribute(ctx: CompCtx): string {
  const locals = [...(ctx.serverLocals ?? [])];

  if (locals.length === 0) {
    return "";
  }

  const entries = locals.map((name) => `${JSON.stringify(name)}: ${name}`).join(", ");

  return ` data-wrn-loop-locals="\${__wrnexusEncodeLoopLocals({ ${entries} })}"`;
}

function unwrapDirectiveExpression(raw: string): string {
  const value = raw.trim();

  if (!value.startsWith("{") || !value.endsWith("}")) {
    return value;
  }

  let depth = 0;
  let quote: '"' | "'" | "`" | null = null;
  let escaped = false;

  for (let index = 0; index < value.length; index++) {
    const char = value[index]!;

    if (escaped) {
      escaped = false;
      continue;
    }

    if (quote) {
      if (char === "\\") {
        escaped = true;
      } else if (char === quote) {
        quote = null;
      }
      continue;
    }

    if (char === '"' || char === "'" || char === "`") {
      quote = char;
      continue;
    }

    if (char === "{") depth++;
    if (char === "}") depth--;

    if (depth === 0 && index < value.length - 1) {
      return value;
    }
  }

  return depth === 0 ? value.slice(1, -1).trim() : value;
}

/** Render a component view node into template-literal-ready source. */
function renderComponentNode(node: ViewNode, ctx: CompCtx): string {
  if (node.type === "text") return compileText(node.value, ctx);

  if (node.type === "each") {
    return renderComponentEachNode(node, ctx);
  }

  if (node.type === "if") {
    return renderComponentIfNode(node, ctx);
  }

  if (node.tag === "Static" || node.tag === "Dynamic") {
    const inner = node.children.map((child) => renderComponentNode(child, ctx)).join("");
    return node.tag === "Static"
      ? inner
      : `<wrn-dynamic-region data-wrn-dynamic="true">${inner}</wrn-dynamic-region>`;
  }

  if (node.tag === "KeepAlive") {
    const key = node.attrs.find((attribute) => attribute.name === "key")?.value ?? "default";
    const inner = node.children.map((child) => renderComponentNode(child, ctx)).join("");
    return `<div data-wrn-keepalive="${compileAttrValue(key, ctx)}">${inner}</div>`;
  }

  if (node.tag === "Portal" || node.tag === "Transition" || node.tag === "Component") {
    const inner = node.children.map((child) => renderComponentNode(child, ctx)).join("");
    const attribute =
      node.tag === "Portal"
        ? "data-wrn-portal"
        : node.tag === "Transition"
          ? "data-wrn-transition"
          : "data-wrn-dynamic-component";
    const source = node.tag === "Portal" ? "to" : node.tag === "Transition" ? "name" : "is";
    const fallback =
      node.tag === "Portal" ? "body" : node.tag === "Transition" ? "wrn-transition" : "";
    const raw = node.attrs.find((item) => item.name === source)?.value ?? fallback;
    return `<div ${attribute}="${compileAttrValue(raw, ctx)}">${inner}</div>`;
  }

  if (isComponentTag(node.tag)) {
    return renderNestedComponentInvocation(node, ctx);
  }

  const loopVariables = loopVarsOf(node);

  const elementContext = {
    ...ctx,
    forwardRestAttrs: false,
    ...(loopVariables.length > 0
      ? { loopVars: new Set([...(ctx.loopVars ?? []), ...loopVariables]) }
      : {}),
  };

  let bindIndex = 0;
  const staticClasses: string[] = [];
  const conditionalClasses: Array<{
    className: string;
    expression: string;
  }> = [];

  for (const attr of node.attrs) {
    if (!attr.event && attr.name === "class") {
      staticClasses.push(attr.value);
    }

    if (!attr.event && attr.name.startsWith("class:")) {
      conditionalClasses.push({
        className: attr.name.slice("class:".length),
        expression: unwrapDirectiveExpression(attr.value),
      });
    }
  }

  const isExplicitComponentMount = node.attrs.some(
    (attribute) => attribute.name === "data-component",
  );

  const attrs = node.attrs
    .filter((a) => a.name !== "class" && !a.name.startsWith("class:"))
    .map((a) => {
      const spread = /^\{\.\.\.([A-Za-z_$][\w$]*)\}$/.exec(a.name);
      if (spread) {
        return `\${__wireSpreadAttrs(${elementContext.resolveExpr(spread[1]!)})}`;
      }

      if (a.event) {
        return ` ${eventAttribute(a.name)}="${escLit(attrEscape(a.value))}"`;
      }

      if (a.boolean) {
        return ` ${a.name}`;
      }

      if (isHtmlBooleanAttribute(a.name)) {
        const expression = wholeAttributeExpression(a.value);
        if (expression) {
          const referencesState = exprRefsComponentReactiveValue(a.value, ctx);
          const referencesLoopVariable = elementContext.loopVars
            ? exprRefsState(a.value, elementContext.loopVars)
            : false;
          const referencesServerLocal = ctx.serverLocals
            ? exprRefsState(a.value, ctx.serverLocals)
            : false;
          const marker =
            referencesState || referencesLoopVariable || referencesServerLocal
              ? ` data-wrn-bind-${bindIndex++}="${escLit(
                  attrEscape(JSON.stringify([a.name, a.value])),
                )}"`
              : "";
          /*
           * A boolean attribute whose expression names a loop variable cannot
           * be resolved on the server: __wireBooleanAttr runs at render time,
           * where `row` or `item` simply does not exist, and the emitted
           * module blew up. Leave the attribute off the server output and let
           * the client bind set it -- the runtime toggles boolean attributes
           * rather than stringifying them, so `checked={isSelected(row)}`
           * behaves correctly once hydrated.
           */
          if (referencesLoopVariable) {
            return ` data-wrn-bind-${bindIndex++}="${escLit(
              attrEscape(JSON.stringify([a.name, a.value])),
            )}"`;
          }
          return `\${__wireBooleanAttr(${JSON.stringify(a.name)}, ${elementContext.resolveExpr(expression)})}${marker}`;
        }

        if (a.value === "false") return "";
        if (a.value === "true" || a.value === "") return ` ${a.name}`;
      }

      const wholeExpression = wholeAttributeExpression(a.value);

      /*
       * data-show carries an EXPRESSION, not a value. The client re-evaluates
       * whatever string it finds in the attribute on every state change, so
       * interpolating `{open || visible}` down to the literal "false" at
       * render time froze the directive: the element could never be shown
       * again, no matter what the state did. A data-wrn-bind marker did not
       * save it either -- the bind rewrites the same attribute the directive
       * reads, and the directive had already captured "false" as its
       * expression. Emitting the expression verbatim (the form Modal uses,
       * data-show="isOpen()") makes both authoring styles behave the same.
       */
      if (a.name === "data-show" && wholeExpression) {
        return ` data-show="${escLit(attrEscape(wholeExpression))}"`;
      }

      const compiledValue =
        isExplicitComponentMount && wholeExpression
          ? `\${__wireProp(${elementContext.resolveExpr(wholeExpression)})}`
          : compileAttrValue(a.value, elementContext);
      const rendered = ` ${a.name}="${compiledValue}"`;

      const referencesState = exprRefsComponentReactiveValue(a.value, ctx);

      const referencesLoopVariable = elementContext.loopVars
        ? exprRefsState(a.value, elementContext.loopVars)
        : false;

      const referencesServerLocal = ctx.serverLocals
        ? exprRefsState(a.value, ctx.serverLocals)
        : false;

      if (
        !a.value.includes("{") ||
        (!referencesState && !referencesLoopVariable && !referencesServerLocal)
      ) {
        return rendered;
      }

      const marker = attrEscape(JSON.stringify([a.name, a.value]));

      return `${rendered} data-wrn-bind-${bindIndex++}="${escLit(marker)}"`;
    })
    .join("");

  const initialConditionalClasses = conditionalClasses
    .map(({ className, expression }) => {
      const referencesLoopVariable = elementContext.loopVars
        ? exprRefsState(expression, elementContext.loopVars)
        : false;

      // data-for variables do not exist during
      // initial server rendering.
      if (referencesLoopVariable) {
        return "";
      }

      return `\${(${ctx.resolveExpr(expression)}) ? ${JSON.stringify(` ${className}`)} : ""}`;
    })
    .join("");

  const staticClassValue = staticClasses.join(" ");

  const classReferencesState = exprRefsComponentReactiveValue(staticClassValue, ctx);
  const classReferencesLoopVariable = elementContext.loopVars
    ? exprRefsState(staticClassValue, elementContext.loopVars)
    : false;
  const classReferencesServerLocal = ctx.serverLocals
    ? exprRefsState(staticClassValue, ctx.serverLocals)
    : false;

  const classHasReactiveExpression =
    staticClassValue.includes("{") &&
    (classReferencesState || classReferencesLoopVariable || classReferencesServerLocal);

  const classAttribute =
    staticClasses.length > 0 || conditionalClasses.length > 0
      ? ` class="${compileAttrValue(staticClassValue, elementContext)}${initialConditionalClasses}"`
      : "";

  const classReactiveBinding = classHasReactiveExpression
    ? ` data-wrn-bind-class="${escLit(attrEscape(JSON.stringify(["class", staticClassValue])))}"`
    : "";

  const classBindings = conditionalClasses
    .map(({ className, expression }, index) => {
      const marker = attrEscape(JSON.stringify([className, expression]));

      return ` data-wrn-class-${index}="${escLit(marker)}"`;
    })
    .join("");

  const loopLocalsAttribute = serverLoopLocalsAttribute(ctx);

  const allAttrs =
    `${loopLocalsAttribute}` +
    `${ctx.forwardRestAttrs ? "${__wireSpreadAttrs(__attrs)}" : ""}` +
    `${
      ctx.eventNames?.length ? ` data-wrn-events="${attrEscape(ctx.eventNames.join(","))}"` : ""
    }` +
    `${classAttribute}` +
    `${classReactiveBinding}` +
    `${classBindings}` +
    `${attrs}`;

  if (VOID_ELEMENTS.has(node.tag.toLowerCase())) {
    return `<${node.tag}${allAttrs}>`;
  }

  const inner = node.children.map((child) => renderComponentNode(child, elementContext)).join("");

  return `<${node.tag}${allAttrs}>${inner}</${node.tag}>`;
}

function generateComponent(ast: PageAst): string {
  const out: string[] = [];
  if (ast.imports.length > 0) out.push(generatedImports(ast).join("\n"));
  const hasServerEach = viewHasServerEach(ast.view);

  const effectiveProps =
    ast.kind === "layout" && !ast.props.some((prop) => prop.name === "content")
      ? [
          {
            name: "content",
            default: '""',
            valueType: "string",
            required: false,
          },
          ...ast.props,
        ]
      : ast.props;

  const browserStates = ast.states.filter((state) => state.runtime !== "server");
  const stateNames = new Set([
    ...browserStates.map((entry) => entry.name),
    ...ast.computed.map((entry) => entry.name),
  ]);
  const nameRefs = new Map<string, string>();
  for (const p of effectiveProps) {
    nameRefs.set(p.name, safeRef(p.name));
  }
  if (!nameRefs.has("attrs")) {
    nameRefs.set("attrs", "__attrs");
  }
  for (const s of ast.states) nameRefs.set(s.name, safeRef(s.name));
  for (const entry of ast.computed) nameRefs.set(entry.name, safeRef(entry.name));
  const resolveExpr = (expr: string): string => {
    let result = expr;
    for (const [name, ref] of nameRefs) {
      if (name !== ref) result = result.replace(new RegExp(`\\b${name}\\b`, "g"), ref);
    }
    return result;
  };
  const ctx: CompCtx = {
    stateNames,
    propNames: new Set(effectiveProps.map((entry) => entry.name)),
    functionNames: new Set(
      ast.runtimeFunctions.filter((fn) => fn.runtime !== "server").map((fn) => fn.name),
    ),
    resolveExpr,
    eventNames: publicOutputNames(ast),
  };

  const serverFunctions = targetFunctions(ast, "server");

  const hasExplicitRestSpread = viewHasRestAttributeSpread(ast.view);
  const rootElementIndex = ast.view.findIndex((node) => node.type === "element");
  const automaticallyForwardRootAttrs =
    !hasExplicitRestSpread &&
    !effectiveProps.some((prop) => prop.name === "attrs") &&
    rootElementIndex >= 0;
  const viewCode = ast.view
    .map((node, index) =>
      renderComponentNode(
        node,
        automaticallyForwardRootAttrs && index === rootElementIndex
          ? { ...ctx, forwardRestAttrs: true }
          : ctx,
      ),
    )
    .join("");
  const styles = ast.styles.map((body) => body.trim()).filter(Boolean);
  const styleTag = escLit(localStyleTag(ast, styles));

  // Props remain server-rendered and also become signals so a parent can drive
  // a mounted child after hydration.
  const behavior = componentBehavior(ast);

  const needsScope =
    ast.runtime !== "server" &&
    (effectiveProps.length > 0 ||
      browserStates.length > 0 ||
      ast.computed.length > 0 ||
      viewHasEvents(ast.view) ||
      behavior !== null);

  if (hasServerEach || needsScope) {
    out.push(`import { Buffer as __WrnexusBuffer } from "node:buffer";`);
  }

  const scopeKeys = [
    ...effectiveProps.map((prop) => prop.name),
    ...browserStates.map((state) => state.name),
  ];

  const behaviorAttr = behaviorAttribute(behavior);

  const decls: string[] = [];
  for (const prop of effectiveProps) {
    if (prop.required) {
      decls.push(
        `  if (__p[${JSON.stringify(prop.name)}] === undefined) throw new TypeError(${JSON.stringify(
          `${ast.name} requires prop '${prop.name}' (${prop.valueType ?? "unknown"})`,
        )});`,
      );
    }
    decls.push(
      `  const ${nameRefs.get(prop.name)}: ${prop.valueType ?? "unknown"} = __coerce(__p[${JSON.stringify(prop.name)}], (${resolveExpr(prop.default)}), ${JSON.stringify(runtimeTypeOf(prop.valueType))}, ${JSON.stringify(prop.name)}) as ${prop.valueType ?? "unknown"};`,
    );
  }
  if (!effectiveProps.some((prop) => prop.name === "attrs")) {
    decls.push(
      `  const __attrs = __restProps(__p, new Set(${JSON.stringify(effectiveProps.map((prop) => prop.name))}));`,
    );
  }
  for (const state of ast.states) {
    decls.push(
      `  let ${nameRefs.get(state.name)}${state.valueType ? `: ${state.valueType}` : ""} = (${resolveExpr(state.expr)});`,
    );
  }
  for (const entry of ast.computed) {
    decls.push(`  const ${nameRefs.get(entry.name)} = (${resolveExpr(entry.expr)});`);
  }

  const returnExpr = needsScope
    ? "`" +
      styleTag +
      `<div data-scope="\${__scope}" data-wrn-scope="\${__scopePayload}"${behaviorAttr}${hydrationAttribute(ast)}>` +
      viewCode +
      "</div>`"
    : "`" + styleTag + viewCode + "`";

  const scopeLine =
    needsScope && scopeKeys.length > 0
      ? `  const __scopeState = { ${scopeKeys
          .map((key) => `${JSON.stringify(key)}: ${nameRefs.get(key)}`)
          .join(
            ", ",
          )} };\n  const __scope = __wrnexusScopeDecl(__scopeState);\n  const __scopePayload = __WrnexusBuffer.from(JSON.stringify(__scopeState), "utf8").toString("base64");\n`
      : needsScope
        ? `  const __scopeState = {};\n  const __scope = "";\n  const __scopePayload = __WrnexusBuffer.from("{}", "utf8").toString("base64");\n`
        : "";

  if (ast.kind === "layout") {
    out.push(`export const __wrnexusLayout = ${JSON.stringify(ast.name)};`);
  } else {
    out.push(`export const __wrnexusComponent = ${JSON.stringify(ast.name)};`);
  }
  out.push(`export const __wrnexusRuntime = ${JSON.stringify(ast.runtime ?? "universal")};`);
  out.push(`export const __wrnexusRender = ${JSON.stringify(ast.renderMode ?? "hybrid")};`);
  out.push(
    `export const __wrnexusHydrate = ${JSON.stringify(["static", "server"].includes(ast.renderMode ?? "") ? "none" : (ast.hydrate ?? "load"))};`,
  );
  out.push(`export const __wrnexusHydrationId = ${JSON.stringify(hydrationId(ast))};`);
  if (Object.keys(ast.cache ?? {}).length > 0)
    out.push(`export const __wrnexusCache = ${JSON.stringify(ast.cache, null, 2)};`);
  if (Object.keys(ast.security).length > 0) {
    out.push(`export const __wrnexusSecurity = ${JSON.stringify(ast.security, null, 2)};`);
  }
  if (Object.keys(ast.navigation).length > 0) {
    out.push(`export const __wrnexusNavigation = ${JSON.stringify(ast.navigation, null, 2)};`);
  }
  const componentStyleExport = localStyleExport(ast, styles);
  if (componentStyleExport) out.push(componentStyleExport);

  if (behavior) {
    out.push(`export const __wrnexusBehavior = ${JSON.stringify(behavior, null, 2)};`);
  }

  const typeSource = ast.types
    .map((body) => body.trim())
    .filter(Boolean)
    .join("\n\n");
  if (typeSource) out.push(typeSource);

  if (effectiveProps.length > 0) {
    out.push(
      `export interface ${ast.name}Props {\n  [attribute: string]: unknown;\n${effectiveProps
        .map(
          (prop) =>
            `  ${JSON.stringify(prop.name)}${prop.required ? "" : "?"}: ${prop.valueType ?? "unknown"};`,
        )
        .join("\n")}\n}`,
    );
  }

  if (ast.outputs.length > 0) {
    out.push(
      `export interface ${ast.name}Outputs {\n${ast.outputs
        .map(
          (output) =>
            `  ${JSON.stringify(output.name)}(${output.payload ? `${output.payload.name}${output.payload.optional ? "?" : ""}: ${output.payload.valueType}` : ""}): void;`,
        )
        .join("\n")}\n}`,
    );
  }

  out.push(`function __coerce(v: unknown, def: unknown, declared: string = "unknown", propName: string = "prop"): unknown {
  if (v === undefined || v === null) {
    return def;
  }

  if (declared === "number" || typeof def === "number") {
    const parsed = Number(v);
    if (!Number.isFinite(parsed)) throw new TypeError("Expected a finite number prop");
    return parsed;
  }

  if (declared === "boolean" || typeof def === "boolean") {
    if (v === true || v === "" || v === "true" || v === 1 || v === "1") return true;
    if (v === false || v === "false" || v === 0 || v === "0") return false;
    throw new TypeError("Expected a boolean prop");
  }

  if (declared === "array" || Array.isArray(def)) {
    if (Array.isArray(v)) {
      return v;
    }

    if (typeof v === "string") {
      try {
        const parsed = JSON.parse(v);
        if (!Array.isArray(parsed)) throw new TypeError("Expected an array prop '" + propName + "'");
        return parsed;
      } catch {
        throw new TypeError("Expected an array prop '" + propName + "'");
      }
    }

    throw new TypeError("Expected an array prop '" + propName + "'");
  }

  if (declared === "object" || (def !== null && typeof def === "object")) {
    if (
      v !== null &&
      typeof v === "object" &&
      !Array.isArray(v)
    ) {
      return v;
    }

    if (typeof v === "string") {
      try {
        const parsed = JSON.parse(v);

        if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
          throw new TypeError("Expected an object prop '" + propName + "'");
        }
        return parsed;
      } catch {
        throw new TypeError("Expected an object prop '" + propName + "'");
      }
    }

    throw new TypeError("Expected an object prop '" + propName + "'");
  }

  if (declared === "bigint") return BigInt(v as string | number | bigint | boolean);
  if (declared === "function" && typeof v !== "function") {
    throw new TypeError("Expected a function prop");
  }
  return declared === "unknown" && def === undefined ? v : String(v);
}

function __restProps(
  props: Record<string, unknown>,
  declared: Set<string>,
): Record<string, unknown> {
  return Object.fromEntries(
    Object.entries(props).filter(([name]) => !declared.has(name)),
  );
}

function __wireHtml(v: unknown): string {
  return String(v == null ? "" : v).replace(
    /[&<>]/g,
    (c) =>
      c === "&"
        ? "&amp;"
        : c === "<"
          ? "&lt;"
          : "&gt;",
  );
}

function __wireAttr(v: unknown): string {
  return String(v == null ? "" : v).replace(
    /[&<>"]/g,
    (c) =>
      c === "&"
        ? "&amp;"
        : c === "<"
          ? "&lt;"
          : c === ">"
            ? "&gt;"
            : "&quot;",
  );
}

function __wireBooleanAttr(name: string, value: unknown): string {
  return value === true ||
    value === "true" ||
    value === "" ||
    value === 1 ||
    value === "1" ||
    value === name
    ? " " + name
    : "";
}

function __wireSpreadAttrs(value: unknown): string {
  if (value === null || typeof value !== "object" || Array.isArray(value)) return "";

  const booleanAttributes = new Set(${JSON.stringify([...HTML_BOOLEAN_ATTRIBUTES])});
  const attributes: string[] = [];

  for (const [name, raw] of Object.entries(value)) {
    const lowerName = name.toLowerCase();
    if (
      !/^[A-Za-z_:][A-Za-z0-9_.:-]*$/.test(name) ||
      lowerName.startsWith("on") ||
      lowerName === "style" ||
      lowerName === "slot" ||
      lowerName === "data-component" ||
      // Internal markers must not leak through a spread. Parent-owned output
      // and prop bindings ride from the mount onto the
      // rendered child root so the mounting scope can bind them there.
      (lowerName.startsWith("data-wrn") &&
        !lowerName.startsWith("data-wrn-out-") &&
        !lowerName.startsWith("data-wrn-prop-bind-"))
    ) {
      continue;
    }

    if (booleanAttributes.has(lowerName)) {
      attributes.push(__wireBooleanAttr(name, raw));
      continue;
    }

    if (raw === false || raw === null || raw === undefined) continue;
    attributes.push(" " + name + '="' + __wireAttr(raw) + '"');
  }

  return attributes.join("");
}

function __wireProp(v: unknown): string {
  const value =
    v !== null && typeof v === "object"
      ? JSON.stringify(v)
      : String(v == null ? "" : v);

  return __wireAttr(value);
}

function __wireRaw(v: unknown): string {
  return String(v == null ? "" : v);
}`);

  if (hasServerEach) {
    out.push(`function __wrnexusEncodeLoopLocals(value: Record<string, unknown>): string {
  return __WrnexusBuffer.from(JSON.stringify(value), "utf8").toString("base64");
}`);
  }

  if (needsScope) {
    out.push(`function __wrnexusSerializeScopeValue(value: unknown): string {
    if (value === undefined) {
      return "undefined";
    }

    if (value === null) {
      return "null";
    }

    if (typeof value === "number") {
      return Number.isFinite(value)
        ? String(value)
        : "null";
    }

    if (typeof value === "boolean") {
      return value ? "true" : "false";
    }

    if (typeof value === "string") {
      return JSON.stringify(value);
    }

    try {
      const serialized = JSON.stringify(value);

      return serialized === undefined
        ? "undefined"
        : serialized;
    } catch {
      return "null";
    }
  }

  function __wrnexusScopeDecl(obj: Record<string, unknown>): string {
    return Object.keys(obj)
      .map(
        (key) =>
          key +
          ": " +
          __wrnexusSerializeScopeValue(
            obj[key],
          ),
      )
      .join(", ")
      .replace(/&/g, "&amp;")
      .replace(/"/g, "&quot;")
      .replace(/</g, "&lt;")
      .replace(/>/g, "&gt;");
  }`);
  }

  const serverFunctionSource = serverFunctions ? `${serverFunctions}\n` : "";

  out.push(
    `export function render(props: ${effectiveProps.length > 0 ? `${ast.name}Props` : "Record<string, unknown>"} = {} as ${effectiveProps.length > 0 ? `${ast.name}Props` : "Record<string, unknown>"}): string {\n` +
      `  const __p = props || {};\n` +
      (decls.length > 0 ? decls.join("\n") + "\n" : "") +
      serverFunctionSource +
      scopeLine +
      `  return ${returnExpr};\n` +
      `}`,
  );

  out.push(
    `export default { name: ${JSON.stringify(ast.name)}, kind: ${JSON.stringify(ast.kind)}, render };`,
  );
  return out.join("\n\n") + "\n";
}

function __wireRaw(v: unknown): string {
  return String(v == null ? "" : v);
}

function wholeAttributeExpression(value: string): string | null {
  const match = /^\s*\{([\s\S]+)\}\s*$/.exec(value);

  return match?.[1]?.trim() || null;
}
function __wireHtml(v: unknown): string {
  return String(v == null ? "" : v).replace(/[&<>]/g, (c) =>
    c === "&" ? "&amp;" : c === "<" ? "&lt;" : "&gt;",
  );
}

function __wireAttr(v: unknown): string {
  return String(v == null ? "" : v).replace(/[&<>"]/g, (c) =>
    c === "&" ? "&amp;" : c === "<" ? "&lt;" : c === ">" ? "&gt;" : "&quot;",
  );
}

function __wireProp(v: unknown): string {
  const value =
    v !== null && typeof v === "object" ? JSON.stringify(v) : String(v == null ? "" : v);

  return __wireAttr(value);
}
function renderPageComponentAttr(attr: Attr, dynamicExpressions: string[]): string {
  if (attr.event) {
    return ` ${componentEventAttribute(attr.name)}="${attrEscape(attr.value)}"`;
  }

  if (attr.boolean) {
    return ` ${attr.name}`;
  }

  const expression = wholeAttributeExpression(attr.value);

  if (!expression) {
    return ` ${attr.name}="${attrEscape(safeAttributeValue(attr.name, attr.value))}"`;
  }

  dynamicExpressions.push(`\${__wrnexusPropAttr(${expression})}`);

  const marker = `\x00WRNEACH${dynamicExpressions.length - 1}\x00`;

  return ` ${attr.name}="${marker}"`;
}
