/**
 * `wrnexus build` — production build (Point 4).
 *
 * Emits into `<appRoot>/dist`:
 *   islands/<name>.js   pre-built, minified island chunks
 *   server.js           a self-contained Bun server with a STATIC manifest of
 *                       every page/api/realtime/middleware module (no runtime
 *                       filesystem scan, no on-the-fly bundling)
 *
 * Run it with:  bun dist/server.js   (PORT env var optional)
 */

import { createHash } from "node:crypto";
import {
  cpSync,
  existsSync,
  mkdirSync,
  readFileSync,
  readdirSync,
  rmSync,
  statSync,
  writeFileSync,
} from "node:fs";
import { basename, dirname, extname, join, relative, resolve } from "node:path";
import { buildRouter, type Route } from "@wrnexus/router";
import { getComponentControllerRuntime, getReactiveRuntime } from "@wrnexus/csr";
import {
  analyzeRuntimeImports,
  analyzeRuntimeRequirements,
  assertValidAst,
  generate,
  generateTargets,
  parse,
  type RuntimeRequirements,
  type DeploymentRuntime,
  runtimeCapabilities,
  resolveWrnImports,
} from "@wrnexus/compiler";
import {
  loadAppConfig,
  headToString,
  renderProductionFontHead,
  findStyleEntry,
  bundleCss,
  renderStyles,
  resolveThemeConfig,
  renderActiveThemeCss,
  renderThemeCss,
  renderThemeRuntime,
} from "@wrnexus/styles";
import { uiComponentsDir, uiCss } from "@wrnexus/ui/registry";
import { renderSchemasScript, type ObjectSchema, type SchemaDescriptor } from "@wrnexus/validation";
import { loadLocales, resolveI18n } from "@wrnexus/i18n";
import { pathToFileURL } from "node:url";
import { checkPerformanceBudgets } from "@wrnexus/core";
import { currentCliVersion } from "./update-notifier.ts";
import {
  createPluginRunner,
  discoverPlugins,
  contentTypeForPath,
  type ClientRuntimeDefinition,
  type PackageAssetDefinition,
  type PluginContributions,
} from "@wrnexus/plugin";

// Import the production server from the package specifier (not a source path) so
// the generated entry resolves whether @wrnexus/dev-server is a workspace or an
// installed dependency. Bun.build bundles it into a self-contained server.js.
const PROD_MODULE = "@wrnexus/dev-server";
const INLINE_CSS_LIMIT_BYTES = 4096;

const fwd = (p: string) => p.replace(/\\/g, "/");

function deploymentRuntime(adapter: string | undefined): DeploymentRuntime | undefined {
  if (!adapter) return "bun";
  if (["bun", "node", "edge", "worker", "service-worker", "browser"].includes(adapter))
    return adapter as DeploymentRuntime;
  return undefined;
}

export function validateRuntimeCapabilities(appRoot: string, adapter?: string): void {
  const runtime = deploymentRuntime(adapter);
  if (!runtime || runtime === "bun" || runtime === "node") return;
  const appDir = join(resolve(appRoot), "app");
  if (!existsSync(appDir)) return;
  const files: string[] = [];
  const walk = (directory: string) => {
    for (const name of readdirSync(directory)) {
      const file = join(directory, name);
      const stat = statSync(file);
      if (stat.isDirectory()) walk(file);
      else if (/\.(?:[cm]?[jt]s|wrn)$/.test(file)) files.push(file);
    }
  };
  walk(appDir);
  const diagnostics = files.flatMap((file) =>
    analyzeRuntimeImports(readFileSync(file, "utf8"), runtime).map(
      (diagnostic) =>
        `${fwd(file.slice(resolve(appRoot).length + 1))}: ${diagnostic.code} ${diagnostic.message}`,
    ),
  );
  if (diagnostics.length)
    throw new Error(`Runtime capability validation failed:\n${diagnostics.join("\n")}`);
}

export async function runBuild(appRoot: string): Promise<void> {
  const root = resolve(appRoot);
  const appDir = join(root, "app");
  const distDir = join(root, "dist");
  const compiledDir = join(distDir, "compiled");
  const clientModulesDir = join(distDir, "client");
  const reactivePath = join(distDir, "reactive.js");
  const controllersPath = join(distDir, "controllers.js");
  const publicDir = join(root, "public");
  const distPublicDir = join(distDir, "public");
  const config = await loadAppConfig(root);
  validateRuntimeCapabilities(root, config.build?.adapter);

  console.log(`Building ${appDir} -> ${distDir}`);

  // Clean output.
  rmSync(distDir, { recursive: true, force: true });
  mkdirSync(compiledDir, { recursive: true });
  mkdirSync(clientModulesDir, { recursive: true });
  if (existsSync(publicDir)) {
    cpSync(publicDir, distPublicDir, { recursive: true });
    console.log(`✓ Public:  ${distPublicDir}`);
  }

  const discoveredPlugins = await discoverPlugins(root, config.plugins, {
    includeDevDependencies: true,
    strict: true,
    warn: (message) => console.warn(`[wrnexus:plugin] ${message}`),
    runtime: deploymentRuntime(config.build?.adapter),
    capabilities: [...runtimeCapabilities(deploymentRuntime(config.build?.adapter) ?? "bun")],
    enforcePermissions: config.pluginPermissions?.enforce,
    grantedPermissions: config.pluginPermissions?.grants,
  });
  const pluginRunner = createPluginRunner(discoveredPlugins, {
    root,
    mode: "production",
    command: "build",
    profile: process.env.WRNEXUS_PROFILE,
    metadata: new Map(),
    warn: (message) => console.warn(`[wrnexus:plugin] ${message}`),
  });
  await pluginRunner.configure(config as Record<string, unknown>);
  await pluginRunner.configResolved(config as Readonly<Record<string, unknown>>);
  const pluginContributions = await pluginRunner.contributions();
  const { generateApplicationTypes } = await import("./types.ts");
  generateApplicationTypes(root, pluginContributions);
  const bundledUiDir = uiComponentsDir();
  const componentDirs = [
    bundledUiDir,
    join(bundledUiDir, "..", "styles"),
    ...pluginContributions.componentDirs,
  ];
  await pluginRunner.hook("buildStart");
  const virtualModules = new Map<string, string>();
  for (const [index, module] of pluginContributions.virtualModules.entries()) {
    const output = join(compiledDir, `virtual-${index}.ts`);
    writeFileSync(
      output,
      await module.load({
        root,
        mode: "production",
        command: "build",
        profile: process.env.WRNEXUS_PROFILE,
        metadata: new Map(),
        warn: (message) => console.warn(`[wrnexus:plugin] ${message}`),
      }),
      "utf8",
    );
    virtualModules.set(module.id, output);
  }

  // `.wrn` route files are compiled once into deterministic intermediate modules.
  // Plugin AST/code transforms run only when configured, so existing applications
  // keep the exact compiler path and output contract by default.
  let compiledCount = 0;
  const compiledFiles = new Map<string, string>();
  const clientFiles = new Map<string, string>();
  const runtimeAnalysis = new Map<string, RuntimeRequirements>();
  const partialStaticFiles = new Set<string>();
  const compileWrn = async (file: string): Promise<void> => {
    if (!file.endsWith(".wrn") || compiledFiles.has(file)) return;
    const source = readFileSync(file, "utf8");
    const artifactId = compiledCount++;
    const out = join(compiledDir, `route${artifactId}.ts`);
    const clientHash = createHash("sha256")
      .update(fwd(file))
      .update("\0")
      .update(source)
      .digest("hex")
      .slice(0, 16);
    const clientOut = join(clientModulesDir, `${basename(file, ".wrn")}-${clientHash}.mjs`);
    const clientUrl = `/__wrnexus/client/${basename(clientOut)}`;
    const clientEntry = join(compiledDir, `client${artifactId}.entry.mjs`);

    // Reserve both artifacts before resolving imports so dependency cycles
    // terminate and every generated import has a deterministic target.
    compiledFiles.set(file, out);
    clientFiles.set(file, clientOut);

    let ast = parse(source);
    assertValidAst(ast, { file, accessibility: true });
    ast = await pluginRunner.transformAst(ast, file);
    if (ast.renderMode === "partial-static") partialStaticFiles.add(file);
    runtimeAnalysis.set(file, analyzeRuntimeRequirements(ast));
    const pluginDiagnostics = await pluginRunner.diagnostics(ast, file);
    const errors = pluginDiagnostics.filter((diagnostic) => diagnostic.severity === "error");
    for (const diagnostic of pluginDiagnostics.filter((item) => item.severity !== "error")) {
      console.warn(`[${diagnostic.code}] ${file}: ${diagnostic.message}`);
    }
    if (errors.length) {
      throw new Error(
        errors.map((diagnostic) => `[${diagnostic.code}] ${diagnostic.message}`).join("\n"),
      );
    }

    try {
      const targets = generateTargets(ast);
      let code = `// compiled from ${fwd(relative(root, file))}\n${generate(ast)}`.replaceAll(
        "__WRNEXUS_CLIENT_MODULE__",
        clientUrl,
      );
      let browserCode = `// browser module compiled from ${fwd(relative(root, file))}\n${targets.browser}`;
      code = await pluginRunner.transformCode(code, file);
      browserCode = await pluginRunner.transformCode(browserCode, file);

      for (const [id, target] of virtualModules) {
        const sourcePattern = id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
        const mainRelative = relative(dirname(out), target).replace(/\\/g, "/");
        const mainSpecifier = mainRelative.startsWith(".") ? mainRelative : `./${mainRelative}`;
        code = code.replace(
          new RegExp(`(["'])${sourcePattern}\\1`, "g"),
          JSON.stringify(mainSpecifier),
        );
        const browserRelative = relative(dirname(clientEntry), target).replace(/\\/g, "/");
        const browserSpecifier = browserRelative.startsWith(".")
          ? browserRelative
          : `./${browserRelative}`;
        browserCode = browserCode.replace(
          new RegExp(`(["'])${sourcePattern}\\1`, "g"),
          JSON.stringify(browserSpecifier),
        );
      }

      const resolvedImports = resolveWrnImports(ast.structuredImports, file, {
        appRoot: root,
        mode: config.imports?.mode ?? "compatible",
        aliases: config.imports?.aliases,
      });
      for (const imported of resolvedImports) {
        if (imported.diagnostic?.severity === "error") {
          throw new Error(`${imported.diagnostic.code}: ${imported.diagnostic.message}`);
        }
        if (!imported.resolved) continue;
        const sourcePattern = imported.declaration.source.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");

        const importAliases = { "@": "./app", ...(config.imports?.aliases ?? {}) };
        const isApplicationImport =
          imported.declaration.source.startsWith(".") ||
          Object.keys(importAliases).some(
            (alias) =>
              imported.declaration.source === alias ||
              imported.declaration.source.startsWith(`${alias}/`),
          );
        if (isApplicationImport) {
          let target = imported.resolved;
          if (target.endsWith(".wrn")) {
            await compileWrn(target);
            target = compiledFiles.get(target)!;
          }
          const relativeTarget = relative(dirname(out), target).replace(/\\/g, "/");
          const specifier = relativeTarget.startsWith(".") ? relativeTarget : `./${relativeTarget}`;
          code = code.replace(
            new RegExp(`(["'])${sourcePattern}\\1`, "g"),
            JSON.stringify(specifier),
          );
        }

        // Browser modules keep only imports referenced by client/shared code.
        // Resolve those imports relative to the original .wrn source before
        // bundling so no filesystem or bare package specifier reaches browsers.
        if (isApplicationImport && new RegExp(`(["'])${sourcePattern}\\1`).test(browserCode)) {
          let browserTarget = imported.resolved;
          if (browserTarget.endsWith(".wrn")) {
            await compileWrn(browserTarget);
            browserTarget = clientFiles.get(browserTarget)!;
          }
          const browserRelative = relative(dirname(clientEntry), browserTarget).replace(/\\/g, "/");
          const browserSpecifier = browserRelative.startsWith(".")
            ? browserRelative
            : `./${browserRelative}`;
          browserCode = browserCode.replace(
            new RegExp(`(["'])${sourcePattern}\\1`, "g"),
            JSON.stringify(browserSpecifier),
          );
        }
      }

      writeFileSync(out, code, "utf8");
      writeFileSync(clientEntry, browserCode, "utf8");
      const browserResult = await Bun.build({
        entrypoints: [clientEntry],
        target: "browser",
        format: "esm",
        splitting: false,
        minify: true,
        sourcemap: config.build?.sourceMaps ? "inline" : "none",
      });
      if (!browserResult.success || !browserResult.outputs.length) {
        throw new Error(
          `WRN-CLIENT-BUNDLE: failed to bundle ${file}\n${browserResult.logs.map(String).join("\n")}`,
        );
      }
      writeFileSync(clientOut, await browserResult.outputs[0]!.text(), "utf8");
      rmSync(clientEntry, { force: true });
    } catch (error) {
      compiledFiles.delete(file);
      clientFiles.delete(file);
      rmSync(out, { force: true });
      rmSync(clientOut, { force: true });
      rmSync(clientEntry, { force: true });
      throw error;
    }
  };
  const importPathFor = (file: string): string => fwd(compiledFiles.get(file) ?? file);
  // Regenerate typed DB queries (app/db/queries/*.sql → queries.gen.ts) first, so
  // any page/API importing them is built against the current SQL.
  const { regenerateQueries } = await import("./db.ts");
  const generated = await regenerateQueries(appDir, config.db?.driver);
  if (generated >= 0) console.log(`✓ Queries: ${generated} (db/queries.gen.ts)`);
  for (const [name, cfg] of Object.entries(config.databases ?? {})) {
    const n = await regenerateQueries(appDir, cfg.driver, name);
    if (n >= 0) console.log(`✓ Queries: ${n} (db/${name}/queries.gen.ts)`);
  }

  // Bundle application and package-owned DB migrations into the build. Package
  // files are namespaced by contribution id so independently installed systems
  // cannot collide with application migration filenames.
  const safeMigrationName = (value: string): string =>
    value.trim().replace(/[^a-zA-Z0-9_.-]+/g, "_") || "migration";
  let hasDefaultMigrations = false;
  const namedMigrationDbs = new Set<string>();
  const migrationTarget = (database?: string): string => {
    const target = database?.trim() || "default";
    if (target === "default") {
      if (!config.db) {
        throw new Error(
          "WRN-PLUGIN-MIGRATION-DATABASE: a package targets the default database, but config.db is not configured.",
        );
      }
      hasDefaultMigrations = true;
      return join(distDir, "migrations");
    }
    if (!config.databases?.[target]) {
      throw new Error(
        `WRN-PLUGIN-MIGRATION-DATABASE: package migration targets unknown database '${target}'.`,
      );
    }
    namedMigrationDbs.add(target);
    return join(distDir, "db", target, "migrations");
  };

  const defaultMigrationsSrc = join(appDir, "db", "migrations");
  if (config.db && existsSync(defaultMigrationsSrc)) {
    cpSync(defaultMigrationsSrc, join(distDir, "migrations"), { recursive: true });
    hasDefaultMigrations = true;
    console.log("✓ Migrations: dist/migrations");
  }
  for (const name of Object.keys(config.databases ?? {})) {
    const source = join(appDir, "db", name, "migrations");
    if (!existsSync(source)) continue;
    cpSync(source, join(distDir, "db", name, "migrations"), { recursive: true });
    namedMigrationDbs.add(name);
    console.log(`✓ Migrations: dist/db/${name}/migrations`);
  }

  for (const migration of pluginContributions.migrations) {
    const destination = migrationTarget(migration.database);
    mkdirSync(destination, { recursive: true });
    const prefix = safeMigrationName(migration.id);
    if (migration.source !== undefined) {
      writeFileSync(join(destination, `${prefix}.sql`), migration.source, "utf8");
      continue;
    }
    if (!migration.entry || !existsSync(migration.entry)) {
      throw new Error(
        `WRN-PLUGIN-MIGRATION-MISSING: ${migration.id} points to ${migration.entry ?? "<empty>"}.`,
      );
    }
    const stat = statSync(migration.entry);
    if (stat.isDirectory()) {
      for (const file of readdirSync(migration.entry)
        .filter((name) => name.endsWith(".sql"))
        .sort()) {
        cpSync(
          join(migration.entry, file),
          join(destination, `${prefix}__${safeMigrationName(file)}`),
        );
      }
    } else if (stat.isFile() && migration.entry.endsWith(".sql")) {
      cpSync(
        migration.entry,
        join(destination, `${prefix}__${safeMigrationName(basename(migration.entry))}`),
      );
    } else {
      throw new Error(
        `WRN-PLUGIN-MIGRATION-ENTRY: ${migration.id} must be a .sql file or directory.`,
      );
    }
  }
  if (pluginContributions.migrations.length) {
    console.log(`✓ Package migrations: ${pluginContributions.migrations.length}`);
  }

  const router = buildRouter(appDir, {
    componentDirs,
    externalRoutes: pluginContributions.routes,
    middlewareFiles: pluginContributions.middleware,
  });
  const wrnFiles = new Set([
    ...router.pages.map((route) => route.file),
    ...router.api.map((route) => route.file),
    ...router.realtime.map((route) => route.file),
    ...router.components.map((component) => component.file),
    ...router.layouts.map((layout) => layout.file),
  ]);
  for (const file of wrnFiles) await compileWrn(file);
  const partialShells = new Map<string, { shell: string; regions: number }>();
  const partialPages = router.pages.filter((route) => partialStaticFiles.has(route.file));
  if (partialPages.length > 0) {
    const { precomputePartialStaticShell } = await import("@wrnexus/dev-server");
    const moduleCache = new Map<string, Record<string, unknown>>();
    const loadCompiled = async (file: string): Promise<Record<string, unknown>> => {
      const existing = moduleCache.get(file);
      if (existing) return existing;
      const output = compiledFiles.get(file);
      if (!output) throw new Error(`WRN-PARTIAL-STATIC-MODULE: ${file} was not compiled`);
      const loaded = (await import(pathToFileURL(output).href)) as Record<string, unknown>;
      moduleCache.set(file, loaded);
      return loaded;
    };
    const components = await Promise.all(
      router.components.map(async (component) => ({
        name: component.name,
        mod: await loadCompiled(component.file),
      })),
    );
    for (const route of partialPages) {
      const result = await precomputePartialStaticShell(await loadCompiled(route.file), components);
      partialShells.set(route.raw, result);
    }
    writeFileSync(
      join(distDir, "partial-shells.json"),
      JSON.stringify(Object.fromEntries(partialShells), null, 2) + "\n",
      "utf8",
    );
    console.log(`✓ Partial shells: ${partialShells.size} build-time route shell(s)`);
  }
  const assetHash = createHash("sha256");
  for (const file of clientFiles.values()) assetHash.update(readFileSync(file));
  if (clientFiles.size) console.log(`✓ Client modules: ${clientFiles.size} bundled module(s)`);
  const emittedPluginAssets = await emitPluginAssets(
    pluginContributions,
    distDir,
    config.build?.sourceMaps === true,
  );
  for (const asset of emittedPluginAssets.assets) assetHash.update(readFileSync(asset.file));
  if (emittedPluginAssets.assets.length) {
    console.log(
      `✓ Plugin assets: ${emittedPluginAssets.assets.length} (${emittedPluginAssets.runtimes.length} runtimes)`,
    );
  }

  // 1) Components are `.wrn` modules rendered server-side — no browser chunks.
  //    They are compiled + statically imported into the manifest below.
  const reactiveCode = await buildBrowserRuntime(
    getReactiveRuntime(),
    reactivePath,
    join(compiledDir, "reactive.entry.js"),
  );
  assetHash.update(reactiveCode);
  console.log(`✓ Runtime: ${reactivePath}`);
  const controllerCode = await buildBrowserRuntime(
    getComponentControllerRuntime(),
    controllersPath,
    join(compiledDir, "controllers.entry.js"),
  );
  assetHash.update(controllerCode);
  console.log(`✓ Controllers: ${controllersPath}`);

  // 1a) Theme tokens + client switcher (always emitted; built-in light/dark).
  const theme = resolveThemeConfig(config.theme);
  const themeCss = renderThemeCss(theme);
  const themeJs = renderThemeRuntime(theme);
  writeFileSync(join(distDir, "theme.css"), themeCss, "utf8");
  writeFileSync(join(distDir, "theme.js"), themeJs, "utf8");
  const themeAssetsDir = join(distDir, "theme");
  for (const themeName of theme.names) {
    const themeDir = join(themeAssetsDir, encodeURIComponent(themeName));
    mkdirSync(themeDir, { recursive: true });
    writeFileSync(join(themeDir, "_.css"), renderActiveThemeCss(theme, themeName), "utf8");
    for (const accentName of theme.accentNames) {
      writeFileSync(
        join(themeDir, `${encodeURIComponent(accentName)}.css`),
        renderActiveThemeCss(theme, themeName, accentName),
        "utf8",
      );
    }
  }
  assetHash.update(themeCss);
  assetHash.update(themeJs);
  console.log(`✓ Theme:   ${theme.names.length} themes (default: ${theme.default})`);

  // 1a2) Wire UI stylesheet (all component classes, themed via tokens).
  const uiStyles = config.styles?.includeUi === false ? "" : uiCss();
  writeFileSync(join(distDir, "ui.css"), uiStyles, "utf8");
  const frameworkStyles = `${themeCss}\n${uiStyles}`;
  writeFileSync(join(distDir, "framework.css"), frameworkStyles, "utf8");
  assetHash.update(uiStyles);
  console.log(
    config.styles?.includeUi === false
      ? `✓ UI:      omitted by styles.includeUi`
      : `✓ UI:      dist/ui.css + framework.css`,
  );

  // 1a3) Validation: bake schema descriptors into the client script.
  const descriptors: Record<string, SchemaDescriptor> = {};
  for (const s of router.schemas) {
    const mod = (await import(pathToFileURL(s.file).href)) as { default?: ObjectSchema };
    if (mod.default && typeof mod.default.describe === "function") {
      descriptors[s.name] = mod.default.describe();
    }
  }
  const schemasJs = renderSchemasScript(descriptors);
  assetHash.update(schemasJs);
  if (router.schemas.length) console.log(`✓ Schemas: ${router.schemas.length}`);

  // 1a4) i18n: bake locale messages into the manifest (opt-in via app/locales).
  const localeMessages = loadLocales(join(appDir, "locales"));
  const i18n = Object.keys(localeMessages).length
    ? resolveI18n(localeMessages, config.i18n)
    : undefined;
  if (i18n) console.log(`✓ i18n:    ${i18n.langs.length} locales (default: ${i18n.default})`);

  // 1b) Build the global stylesheet, if any.
  const styleEntry = findStyleEntry(appDir, root, config.styles?.entry);
  let hasStyles = false;
  let inlineStyles = "";
  const hasPackageStyleEntries = pluginContributions.styles.some((style) => !!style.entry);
  if (styleEntry || hasPackageStyleEntries) {
    const css = await renderStyles(
      {
        entryPath: styleEntry,
        appDir,
        appRoot: root,
        mode: "production",
        sources: [
          ...componentDirs,
          ...pluginContributions.styles.flatMap((style) => (style.source ? [style.source] : [])),
        ],
        entries: pluginContributions.styles.flatMap((style) => (style.entry ? [style.entry] : [])),
      },
      config.styles,
    );
    // Theme tokens are request-selected and loaded separately. Keep only the
    // shared UI primitives with application CSS so the full theme/accent
    // matrix cannot leak back into the blocking stylesheet.
    const combinedSource = `${uiStyles}\n${css}`;
    const combinedInput = join(distDir, ".wrnexus-combined.css");
    writeFileSync(combinedInput, combinedSource, "utf8");
    const combinedCss = await bundleCss(combinedInput, "production");
    rmSync(combinedInput, { force: true });
    assetHash.update(combinedCss);
    // One shared blocking request for UI + application CSS. The small active
    // theme stylesheet is selected from the request cookie and loaded first.
    writeFileSync(join(distDir, "styles.css"), combinedCss, "utf8");
    hasStyles = true;
    if (Buffer.byteLength(combinedCss, "utf8") <= INLINE_CSS_LIMIT_BYTES) {
      inlineStyles = combinedCss;
    }
    console.log(`✓ Styles:  ${join(distDir, "styles.css")}`);
  }
  const assetVersion = assetHash.digest("hex").slice(0, 12);
  const headStr = [await renderProductionFontHead(config.fonts), headToString(config.head)]
    .filter(Boolean)
    .join("\n    ");

  // 2) Generate a server entry with STATIC imports + a manifest.
  const imports: string[] = [];
  let counter = 0;

  // Authorization: emit a small side-effecting module that statically imports
  // every app/authz/*.ts declaration and calls setAuthzCatalog EAGERLY, then
  // import THAT MODULE FIRST — before pages/api/realtime/middleware/
  // components/layouts — so it runs before any other static import's module
  // body, including app middleware that reads getAuthzCatalog() at module
  // scope (the same eager shape authzMiddleware({ catalog, ... }) itself
  // requires; app/middleware/logger.ts's `export default requestLogger({...})`
  // is the same pattern). ES modules evaluate every static import before the
  // importing module's own body runs, and evaluate sibling imports in
  // declaration order — so import POSITION is evaluation order, and this
  // must be imports[0], strictly before every other push into `imports`
  // below (in particular before any `mw*` import). This module is
  // deliberately silent about a missing default export (see
  // applyAuthzManifestEarly in @wrnexus/dev-server): createProductionHandlers
  // performs the identical merge again, with its warnings, as an idempotent
  // second pass — both for adapters that bypass this generated entry and to
  // avoid warning twice about the same declaration in the normal path.
  {
    let authzSetupCounter = 0;
    const authzSetupImports: string[] = [];
    const authzSetupEntries = router.authz
      .map((a) => {
        const v = `d${authzSetupCounter++}`;
        authzSetupImports.push(`import * as ${v} from ${JSON.stringify(fwd(a.file))};`);
        return `{ source: ${JSON.stringify(fwd(a.file))}, module: ${v}.default }`;
      })
      .join(", ");
    const authzSetupContent = `// AUTO-GENERATED authz catalog setup — do not edit.
// Imported FIRST by the production entry (see the "Authorization" comment
// there) so getAuthzCatalog() is populated before any other static import's
// module body runs.
import { applyAuthzManifestEarly } from "@wrnexus/dev-server";
${authzSetupImports.join("\n")}

applyAuthzManifestEarly([${authzSetupEntries}]);
`;
    writeFileSync(join(distDir, ".authz-setup.ts"), authzSetupContent, "utf8");
    imports.push(`import "./.authz-setup.ts";`);
  }

  const manifestRoutes = (routes: Route[]): string => {
    const parts = routes.map((r) => {
      const v = `m${counter++}`;
      imports.push(`import * as ${v} from ${JSON.stringify(importPathFor(r.file))};`);
      const partial = partialShells.get(r.raw);
      return `    { raw: ${JSON.stringify(r.raw)}, mod: ${v}${partial ? `, staticShell: ${JSON.stringify(partial.shell)}` : ""} },`;
    });
    return parts.length ? `\n${parts.join("\n")}\n  ` : "";
  };

  const pagesLit = manifestRoutes(router.pages);
  const apiLit = manifestRoutes(router.api);
  const realtimeLit = manifestRoutes(router.realtime);

  const mwVars = router.middlewareFiles.map((file) => {
    const v = `mw${counter++}`;
    imports.push(`import ${v} from ${JSON.stringify(fwd(file))};`);
    return v;
  });

  // Components: compile each `.wrn` to a module and statically import it,
  // keyed by name so the production runtime can render it on demand.
  const componentsLit = router.components
    .map((c) => {
      const v = `c${counter++}`;
      imports.push(`import { render as ${v} } from ${JSON.stringify(importPathFor(c.file))};`);
      return `{ name: ${JSON.stringify(c.name)}, mod: { render: ${v} } }`;
    })
    .join(", ");
  console.log(`✓ Components: ${router.components.length}`);

  // Named page layouts (app/layouts/*.wrn), compiled + imported like components.
  const layoutsLit = router.layouts
    .map((l) => {
      const v = `c${counter++}`;
      imports.push(`import { render as ${v} } from ${JSON.stringify(importPathFor(l.file))};`);
      return `{ name: ${JSON.stringify(l.name)}, mod: { render: ${v} } }`;
    })
    .join(", ");
  if (router.layouts.length) console.log(`✓ Layouts: ${router.layouts.length}`);

  // RPC services are server-only modules. Production statically imports them
  // so the runtime can dispatch private calls without filesystem discovery.
  const servicesLit = router.services
    .map((service) => {
      const v = `s${counter++}`;
      imports.push(`import * as ${v} from ${JSON.stringify(fwd(service.file))};`);
      return `{ name: ${JSON.stringify(service.name)}, mod: ${v} }`;
    })
    .join(", ");
  if (router.services.length) console.log(`✓ RPC services: ${router.services.length}`);

  // Authorization declarations again, this time for ProdOptions.authz — a
  // SEPARATE set of static imports of the exact same files (harmless; ES
  // modules are evaluated once and shared across every importer), statically
  // imported like components/layouts — NOT baked into JSON like schemasJs,
  // because the catalog contains policy FUNCTIONS, which JSON.stringify
  // cannot carry. Each module is passed through by reference and merged
  // AGAIN into the process-wide catalog by createProductionHandlers's second
  // pass (prod.ts) — see the ".authz-setup.ts" block above for the EARLY,
  // eager pass that actually makes the catalog visible to app middleware. A
  // file with no default export becomes `module: undefined` here;
  // createProductionHandlers warns and skips it, matching the dev loader
  // (authz-boot.ts).
  const authzLit = router.authz
    .map((a) => {
      const v = `az${counter++}`;
      imports.push(`import * as ${v} from ${JSON.stringify(fwd(a.file))};`);
      return `{ source: ${JSON.stringify(fwd(a.file))}, module: ${v}.default }`;
    })
    .join(", ");
  if (router.authz.length) console.log(`✓ Authz: ${router.authz.length} declaration(s)`);

  const entry = `// AUTO-GENERATED production server entry — do not edit.
import { join } from "node:path";
import { createProductionServer } from ${JSON.stringify(PROD_MODULE)};
${imports.join("\n")}

await createProductionServer(
  {
    pages: [${pagesLit}],
    api: [${apiLit}],
    realtime: [${realtimeLit}],
    middleware: [${mwVars.join(", ")}],
    components: [${componentsLit}],
    layouts: [${layoutsLit}],
    services: [${servicesLit}],
  },
  {
    reactivePath: join(import.meta.dir, "reactive.js"),
    controllersPath: join(import.meta.dir, "controllers.js"),
    clientModulesDir: join(import.meta.dir, "client"),
    themePath: join(import.meta.dir, "theme.css"),
    themeAssetsDir: join(import.meta.dir, "theme"),
    themeJsPath: join(import.meta.dir, "theme.js"),
    theme: ${JSON.stringify(theme)},
    uiCssPath: join(import.meta.dir, "ui.css"),
    frameworkCssPath: join(import.meta.dir, "framework.css"),
    schemasJs: ${JSON.stringify(schemasJs)},
    authz: [${authzLit}],
    i18n: ${i18n ? JSON.stringify(i18n) : "undefined"},
    db: ${config.db ? JSON.stringify(config.db) : "undefined"},
    databases: ${config.databases ? JSON.stringify(config.databases) : "undefined"},
    storage: ${config.storage ? JSON.stringify(config.storage) : "undefined"},
    ${hasDefaultMigrations ? `migrationsDir: join(import.meta.dir, "migrations"),` : ""}
    ${
      namedMigrationDbs.size
        ? `databaseMigrationDirs: { ${[...namedMigrationDbs]
            .map(
              (n) =>
                `${JSON.stringify(n)}: join(import.meta.dir, "db", ${JSON.stringify(n)}, "migrations")`,
            )
            .join(", ")} },`
        : ""
    }
    realtime: ${config.realtime ? JSON.stringify(config.realtime) : "undefined"},
    publicDir: join(import.meta.dir, "public"),
    ${hasStyles ? `stylesPath: join(import.meta.dir, "styles.css"),` : ""}
    ${hasStyles ? `stylesIncludeUi: true,` : ""}
    ${inlineStyles ? `inlineStyles: ${JSON.stringify(inlineStyles)},` : ""}
    assetVersion: ${JSON.stringify(assetVersion)},
    clientRuntimes: ${JSON.stringify(emittedPluginAssets.runtimes)},
    pluginAssets: ${renderProductionPluginAssets(emittedPluginAssets.assets)},
    head: ${JSON.stringify(headStr)},
    seo: ${JSON.stringify(config.seo ?? {})},
    mobile: ${JSON.stringify(config.mobile ?? {})},
    pwa: ${JSON.stringify(config.pwa ?? {})},
    security: ${JSON.stringify(config.security ?? {})},
    observability: ${JSON.stringify(config.observability ?? {})},
    tenancy: ${JSON.stringify(config.tenancy ?? {})},
    navigation: ${JSON.stringify(config.navigation ?? {})},
    developmentRuntime: process.env.WRNEXUS_PRODUCTION_DEV === "1",
  },
);
`;

  const entryPath = join(distDir, ".server-entry.ts");
  writeFileSync(entryPath, entry, "utf8");

  // 3) Bundle the entry into a single self-contained, minified server.js
  //    (target bun). This also minifies every bundled page/component/route module.
  const result = await Bun.build({
    entrypoints: [entryPath],
    target: "bun",
    format: "esm",
    minify: true,
    sourcemap: config.build?.sourceMaps ? "inline" : "none",
  });
  if (!result.success) {
    throw new Error("Server build failed:\n" + result.logs.map(String).join("\n"));
  }
  writeFileSync(join(distDir, "server.js"), await result.outputs[0]!.text(), "utf8");

  const report = createBuildReport({
    root,
    distDir,
    publicDir: distPublicDir,
    adapter: config.build?.adapter ?? "bun",
    routes: [
      ...router.pages.map((route) => ({ kind: "page" as const, route })),
      ...router.api.map((route) => ({ kind: "api" as const, route })),
      ...router.realtime.map((route) => ({ kind: "realtime" as const, route })),
    ],
    runtimeFile: reactivePath,
    cssFile: hasStyles ? join(distDir, "styles.css") : join(distDir, "framework.css"),
    runtimeAnalysis,
  });
  report.pluginAssets = emittedPluginAssets.assets.map((asset) => ({
    id: asset.id,
    publicPath: asset.publicPath,
    bytes: statSync(asset.file).size,
    runtime: asset.runtime,
  }));
  report.plugins = pluginRunner.plugins.map((plugin) => ({
    name: plugin.name,
    version: plugin.version,
  }));
  report.clientRuntimes = emittedPluginAssets.runtimes.map((runtime) => ({
    id: runtime.id,
    publicPath: runtime.publicPath!,
    type: runtime.type ?? "module",
    load: runtime.load ?? "defer",
  }));
  report.migrations = pluginContributions.migrations.map((migration) => ({
    id: migration.id,
    database: migration.database ?? "default",
    source: migration.entry ?? "inline",
  }));
  report.componentDirs = componentDirs.map(fwd);
  report.partialStaticShells = [...partialShells].map(([route, value]) => ({
    route,
    regions: value.regions,
    bytes: Buffer.byteLength(value.shell, "utf8"),
  }));
  const violations = checkPerformanceBudgets(
    config.performance?.budgets ?? {},
    report.measurements,
  );
  report.budgetViolations = violations;
  writeFileSync(join(distDir, "build-report.json"), JSON.stringify(report, null, 2) + "\n", "utf8");
  const contributedAdapter = pluginContributions.deploymentAdapters.find(
    (adapter) => adapter.name === config.build?.adapter,
  );
  if (contributedAdapter)
    await contributedAdapter.build(
      { distDir, report },
      {
        root,
        mode: "production",
        command: "build",
        profile: process.env.WRNEXUS_PROFILE,
        metadata: new Map(),
        warn: (message) => console.warn(`[wrnexus:plugin] ${message}`),
      },
    );
  await pluginRunner.hook("buildEnd", report);

  console.log(`✓ Server:  ${join(distDir, "server.js")}`);
  console.log(
    `✓ Routes:  ${router.pages.length} pages, ${router.api.length} api, ${router.realtime.length} realtime, ${mwVars.length} middleware`,
  );
  console.log(`✓ Report:  ${join(distDir, "build-report.json")}`);
  if (violations.length) {
    for (const violation of violations) {
      console.warn(
        `⚠ Budget ${violation.metric}: ${violation.actual} > ${violation.budget} (+${violation.overBy})`,
      );
    }
    if (config.performance?.enforcement === "error") {
      throw new Error(
        `WRN-PERFORMANCE-BUDGET: ${violations.length} production budget(s) exceeded.`,
      );
    }
  }
  console.log(`\nRun it:  bun ${fwd(join(distDir, "server.js"))}`);
}

interface EmittedPluginAsset {
  id: string;
  publicPath: string;
  file: string;
  contentType: string;
  runtime: boolean;
  immutable: boolean;
}

interface EmittedPluginAssets {
  assets: EmittedPluginAsset[];
  runtimes: ClientRuntimeDefinition[];
}

function safeAssetName(id: string): string {
  return (
    id
      .replace(/^@/, "")
      .replace(/[^A-Za-z0-9_-]+/g, "-")
      .replace(/^-+|-+$/g, "") || "asset"
  );
}

function sourceBytes(source: string | Uint8Array): Uint8Array {
  return typeof source === "string" ? new TextEncoder().encode(source) : source;
}

async function compileRuntimeSource(
  runtime: ClientRuntimeDefinition,
  sourceMaps: boolean,
): Promise<Uint8Array> {
  if (runtime.source !== undefined) return sourceBytes(runtime.source);
  if (!runtime.entry) throw new Error(`Runtime '${runtime.id}' has no entry.`);
  const shouldBundle = runtime.bundle ?? /\.[cm]?tsx?$/.test(runtime.entry);
  if (!shouldBundle) return readFileSync(runtime.entry);
  const result = await Bun.build({
    entrypoints: [runtime.entry],
    target: "browser",
    format: runtime.type === "script" ? "iife" : "esm",
    minify: true,
    sourcemap: sourceMaps ? "inline" : "none",
  });
  if (!result.success || !result.outputs[0]) {
    throw new Error(
      `Client runtime '${runtime.id}' failed to build:\n${result.logs.map(String).join("\n")}`,
    );
  }
  return new Uint8Array(await result.outputs[0].arrayBuffer());
}

async function rawAssetSource(asset: PackageAssetDefinition): Promise<Uint8Array> {
  if (asset.source !== undefined) return sourceBytes(asset.source);
  if (!asset.entry) throw new Error(`Asset '${asset.id}' has no entry.`);
  return readFileSync(asset.entry);
}

async function emitPluginAssets(
  contributions: PluginContributions,
  distDir: string,
  sourceMaps: boolean,
): Promise<EmittedPluginAssets> {
  const outputDir = join(distDir, "plugin-assets");
  mkdirSync(outputDir, { recursive: true });
  const assets: EmittedPluginAsset[] = [];
  const runtimes: ClientRuntimeDefinition[] = [];

  for (const runtime of contributions.clientRuntimes) {
    const bytes = await compileRuntimeSource(runtime, sourceMaps);
    const hash = createHash("sha256").update(bytes).digest("hex").slice(0, 12);
    const filename = `${safeAssetName(runtime.id)}.${hash}.js`;
    const file = join(outputDir, filename);
    writeFileSync(file, bytes);
    const publicPath = `/__wrnexus/assets/${filename}`;
    assets.push({
      id: runtime.id,
      publicPath,
      file,
      contentType: "text/javascript; charset=utf-8",
      runtime: true,
      immutable: true,
    });
    runtimes.push({ ...runtime, entry: undefined, source: undefined, publicPath });
  }

  for (const asset of contributions.assets) {
    const bytes = await rawAssetSource(asset);
    const hash = createHash("sha256").update(bytes).digest("hex").slice(0, 12);
    const extension = extname(asset.entry ?? asset.publicPath ?? "") || "";
    const filename = `${safeAssetName(asset.id)}.${hash}${extension}`;
    const file = join(outputDir, filename);
    writeFileSync(file, bytes);
    // Package assets keep their declared public URL so component markup, CSS,
    // and server responses do not need build-time string rewriting. The disk
    // filename is still content-addressed to make deployments atomic.
    assets.push({
      id: asset.id,
      publicPath: asset.publicPath!,
      file,
      contentType: asset.contentType ?? contentTypeForPath(asset.entry ?? asset.publicPath ?? ""),
      runtime: false,
      immutable: asset.immutable ?? false,
    });
  }

  return { assets, runtimes };
}

function renderProductionPluginAssets(assets: readonly EmittedPluginAsset[]): string {
  if (!assets.length) return "{}";
  const entries = assets.map(
    (asset) =>
      `${JSON.stringify(asset.publicPath)}: { path: join(import.meta.dir, "plugin-assets", ${JSON.stringify(asset.file.split(/[\\/]/).pop())}), contentType: ${JSON.stringify(asset.contentType)}, immutable: ${asset.immutable} }`,
  );
  return `{ ${entries.join(", ")} }`;
}

interface BuildReport {
  frameworkVersion: string;
  plugins?: Array<{ name: string; version?: string }>;
  pluginAssets?: Array<{ id: string; publicPath: string; bytes: number; runtime: boolean }>;
  clientRuntimes?: Array<{ id: string; publicPath: string; type: string; load: string }>;
  migrations?: Array<{ id: string; database: string; source: string }>;
  componentDirs?: string[];
  partialStaticShells?: Array<{ route: string; regions: number; bytes: number }>;
  generatedAt: string;
  root: string;
  adapter: string;
  routes: Array<{
    kind: "page" | "api" | "realtime";
    path: string;
    source: string;
    sourceBytes: number;
    dynamicParams: string[];
    execution: RuntimeRequirements["kind"];
    canPrerender: boolean;
    needsClientRuntime: boolean;
    needsServerRuntime: boolean;
    hydrationStrategy: string | null;
    reasons: string[];
    optimization: RuntimeRequirements["optimization"];
    cachePolicy: Record<string, string>;
    requiredPermission: string | null;
  }>;
  assets: Array<{ file: string; bytes: number }>;
  measurements: { routeJsBytes: number; routeCssBytes: number; imageBytes: number };
  budgetViolations: ReturnType<typeof checkPerformanceBudgets>;
}

function fileBytes(file: string): number {
  return existsSync(file) && statSync(file).isFile() ? statSync(file).size : 0;
}

function walkFiles(dir: string): string[] {
  if (!existsSync(dir)) return [];
  const files: string[] = [];
  for (const entry of readdirSync(dir)) {
    const path = join(dir, entry);
    const stat = statSync(path);
    if (stat.isDirectory()) files.push(...walkFiles(path));
    else if (stat.isFile()) files.push(path);
  }
  return files;
}

function createBuildReport(input: {
  root: string;
  distDir: string;
  publicDir: string;
  adapter: string;
  routes: Array<{ kind: "page" | "api" | "realtime"; route: Route }>;
  runtimeFile: string;
  cssFile: string;
  runtimeAnalysis: ReadonlyMap<string, RuntimeRequirements>;
}): BuildReport {
  const assets = walkFiles(input.distDir)
    .filter((file) => !file.endsWith("build-report.json") && !fwd(file).includes("/compiled/"))
    .map((file) => ({ file: fwd(file.slice(input.distDir.length + 1)), bytes: fileBytes(file) }))
    .sort((a, b) => b.bytes - a.bytes);
  const imageExtensions = /\.(?:avif|gif|jpe?g|png|svg|webp)$/i;
  const imageBytes = Math.max(
    0,
    ...walkFiles(input.publicDir)
      .filter((file) => imageExtensions.test(file))
      .map(fileBytes),
  );
  return {
    frameworkVersion: currentCliVersion(),
    generatedAt: new Date().toISOString(),
    root: input.root,
    adapter: input.adapter,
    routes: input.routes.map(({ kind, route }) => ({
      kind,
      path: route.raw,
      source: fwd(route.file.replace(input.root, "").replace(/^\//, "")),
      sourceBytes: fileBytes(route.file),
      dynamicParams: route.paramNames,
      execution: input.runtimeAnalysis.get(route.file)?.kind ?? "dynamic",
      canPrerender: input.runtimeAnalysis.get(route.file)?.canPrerender ?? false,
      needsClientRuntime: input.runtimeAnalysis.get(route.file)?.needsClientRuntime ?? true,
      needsServerRuntime: input.runtimeAnalysis.get(route.file)?.needsServerRuntime ?? true,
      hydrationStrategy: input.runtimeAnalysis.get(route.file)?.hydrationStrategy ?? null,
      reasons: input.runtimeAnalysis.get(route.file)?.reasons ?? [
        "runtime requirements unavailable",
      ],
      optimization: input.runtimeAnalysis.get(route.file)?.optimization ?? {
        staticNodes: 0,
        reactiveRegions: 0,
        eliminatedBranches: 0,
        unusedState: [],
        unusedHandlers: [],
        constantProps: [],
        unusedLocalCssClasses: [],
        batchableStateUpdates: 0,
        memoizableComponents: [],
        preloadDependencies: [],
        serverOnlyModules: [],
      },
      cachePolicy: input.runtimeAnalysis.get(route.file)?.cachePolicy ?? {},
      requiredPermission: input.runtimeAnalysis.get(route.file)?.requiredPermission ?? null,
    })),
    assets,
    measurements: {
      routeJsBytes: fileBytes(input.runtimeFile),
      routeCssBytes: fileBytes(input.cssFile),
      imageBytes,
    },
    budgetViolations: [],
  };
}

async function buildBrowserRuntime(
  source: string,
  outFile: string,
  entryFile: string,
): Promise<string> {
  writeFileSync(entryFile, source, "utf8");
  const result = await Bun.build({
    entrypoints: [entryFile],
    target: "browser",
    format: "esm",
    minify: true,
  });
  if (!result.success) {
    throw new Error("Runtime build failed:\n" + result.logs.map(String).join("\n"));
  }
  const code = await result.outputs[0]!.text();
  writeFileSync(outFile, code, "utf8");
  return code;
}
