/**
 * `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, rmSync, writeFileSync } from "node:fs";
import { join, resolve } from "node:path";
import { buildRouter, type Route } from "@wrnexus/router";
import { getReactiveRuntime } from "@wrnexus/csr";
import { compileWireFile } from "@wrnexus/compiler";
import {
  loadAppConfig,
  headToString,
  renderProductionFontHead,
  findStyleEntry,
  renderStyles,
  resolveThemeConfig,
  renderThemeCss,
  renderThemeRuntime,
} from "@wrnexus/styles";
import { uiComponentsDir, uiCss } from "@wrnexus/ui";
import { renderSchemasScript, type ObjectSchema, type SchemaDescriptor } from "@wrnexus/validation";
import { loadLocales, resolveI18n } from "@wrnexus/i18n";
import { pathToFileURL } from "node:url";

// 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, "/");

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 reactivePath = join(distDir, "reactive.js");
  const publicDir = join(root, "public");
  const distPublicDir = join(distDir, "public");

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

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

  // `.wrn` route files are compiled to `.ts` so Bun.build can bundle them.
  let compiledCount = 0;
  const importPathFor = (file: string): string => {
    if (!file.endsWith(".wrn")) return fwd(file);
    const ts = compileWireFile(readFileSync(file, "utf8"));
    const out = join(compiledDir, `route${compiledCount++}.ts`);
    writeFileSync(out, ts, "utf8");
    return fwd(out);
  };

  const config = await loadAppConfig(root);
  // 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 DB migrations into the build so the production server can auto-apply
  // them on startup (dev auto-migrates from app/db/migrations; prod needs the
  // .sql files inside dist/). The default db's migrations go to dist/migrations;
  // each named db's to dist/db/<name>/migrations.
  const defaultMigrationsSrc = join(appDir, "db", "migrations");
  const hasDefaultMigrations = !!config.db && existsSync(defaultMigrationsSrc);
  if (hasDefaultMigrations) {
    cpSync(defaultMigrationsSrc, join(distDir, "migrations"), { recursive: true });
    console.log(`✓ Migrations: dist/migrations`);
  }
  const namedMigrationDbs: string[] = [];
  for (const name of Object.keys(config.databases ?? {})) {
    const src = join(appDir, "db", name, "migrations");
    if (!existsSync(src)) continue;
    cpSync(src, join(distDir, "db", name, "migrations"), { recursive: true });
    namedMigrationDbs.push(name);
    console.log(`✓ Migrations: dist/db/${name}/migrations`);
  }

  const router = buildRouter(appDir, { componentDirs: [uiComponentsDir()] });
  const assetHash = createHash("sha256");

  // 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}`);

  // 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");
  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 = 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(`✓ 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 = "";
  if (styleEntry) {
    const css = await renderStyles(
      { entryPath: styleEntry, appDir, appRoot: root, mode: "production" },
      config.styles,
    );
    assetHash.update(css);
    const combinedCss = `${frameworkStyles}\n${css}`;
    // One blocking CSS request in production: tokens → UI → application CSS.
    // The standalone framework.css remains available for apps without global CSS.
    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;

  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))};`);
      return `    { raw: ${JSON.stringify(r.raw)}, mod: ${v} },`;
    });
    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 * as ${v} from ${JSON.stringify(importPathFor(c.file))};`);
      return `{ name: ${JSON.stringify(c.name)}, mod: ${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 * as ${v} from ${JSON.stringify(importPathFor(l.file))};`);
      return `{ name: ${JSON.stringify(l.name)}, mod: ${v} }`;
    })
    .join(", ");
  if (router.layouts.length) console.log(`✓ Layouts: ${router.layouts.length}`);

  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}],
  },
  {
    reactivePath: join(import.meta.dir, "reactive.js"),
    themePath: join(import.meta.dir, "theme.css"),
    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)},
    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.length
        ? `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 ? `stylesIncludeFramework: true,` : ""}
    ${inlineStyles ? `inlineStyles: ${JSON.stringify(inlineStyles)},` : ""}
    assetVersion: ${JSON.stringify(assetVersion)},
    head: ${JSON.stringify(headStr)},
    seo: ${JSON.stringify(config.seo ?? {})},
    mobile: ${JSON.stringify(config.mobile ?? {})},
    pwa: ${JSON.stringify(config.pwa ?? {})},
    security: ${JSON.stringify(config.security ?? {})},
  },
);
`;

  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,
  });
  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");

  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(`\nRun it:  bun ${fwd(join(distDir, "server.js"))}`);
}

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;
}
