/**
 * @wrnexus/dev-server/prod — the production server (Point 4).
 *
 * Unlike dev, there is NO filesystem scan and NO on-the-fly bundling at runtime.
 * `wrnexus build` generates an entry that statically imports every route and
 * component module and hands them here as a manifest. We rebuild the (cheap)
 * route-matching tables from the raw patterns and run the exact same request
 * runtime as dev. Normal preview/deploy output has no live-reload client; the
 * supervised `dev --production-runtime` mode can explicitly enable it.
 */

import { join } from "node:path";
import type { Middleware, Mode, SecurityConfig, SeoConfig } from "@wrnexus/core";
import {
  compileRoutePattern,
  matchRoute,
  sortRoutes,
  type Route,
  type Router,
} from "@wrnexus/router";
import {
  getActionRuntime,
  getComponentControllerRuntime,
  getReactiveRuntime,
  getNavRuntime,
  getRealtimeRuntime,
} from "@wrnexus/csr";
import {
  loadEnv,
  resolveProfile,
  type ResolvedTheme,
  type MobileConfig,
  type PwaConfig,
  type ObservabilityConfig,
  type TenancyConfig,
  type NavigationConfig,
} from "@wrnexus/styles";
import { VALIDATE_RUNTIME } from "@wrnexus/validation";
import { I18N_RUNTIME, type ResolvedI18n } from "@wrnexus/i18n";
import { setDb, registerLazyDb, getDb, hasDb, migrate } from "@wrnexus/db";
import { connectFromConfig } from "@wrnexus/db/connect";
import { hasAuthzCatalog, mergeCatalogs, setAuthzCatalog, type AuthzModule } from "@wrnexus/authz";
import {
  configureStorage,
  serveStoredFile,
  UPLOAD_RUNTIME,
  UPLOAD_JS_HREF,
  UPLOADS_PREFIX,
  type StorageConfig,
} from "@wrnexus/uploader";
import { realtimeBusFromConfig } from "./realtime-bus.ts";
import { createHandlers, type AssetServer, type WsData } from "./runtime.ts";
import { servePublicAsset } from "./public.ts";
import type { ClientRuntimeDefinition } from "@wrnexus/plugin";
import { HmrHub } from "./hmr.ts";

type RouteModule = Record<string, unknown>;

export interface ManifestRoute {
  /** URL pattern, e.g. `/users/[id]`. */
  raw: string;
  /** The statically-imported route module. */
  mod: RouteModule;
  /** Body shell precomputed by `wrnexus build` for a partial-static page. */
  staticShell?: string;
}

export interface ProdManifest {
  pages: ManifestRoute[];
  api: ManifestRoute[];
  realtime: ManifestRoute[];
  middleware: Middleware[];
  /** Server-rendered components, statically imported and keyed by name. */
  components: { name: string; mod: RouteModule }[];
  /** Named page layouts (from app/layouts/*.wrn). */
  layouts: { name: string; mod: RouteModule }[];
  /** RPC service implementations (from app/services/*.ts). */
  services?: { name: string; mod: RouteModule }[];
}

export interface ProductionPluginAsset {
  path: string;
  contentType: string;
  immutable?: boolean;
}

export interface ProdOptions {
  /** Absolute path to the pre-built global stylesheet, if any. */
  stylesPath?: string;
  /** Small production stylesheet inlined into the document head. */
  inlineStyles?: string;
  /** `stylesPath` contains theme + UI + app CSS in cascade order. */
  stylesIncludeFramework?: boolean;
  /** `stylesPath` already contains the shared Wire UI stylesheet. */
  stylesIncludeUi?: boolean;
  /** Absolute path to the pre-built reactive runtime. */
  reactivePath?: string;
  /** Absolute path to the on-demand component controller runtime. */
  controllersPath?: string;
  /** Absolute directory containing bundled per-WRN browser modules. */
  clientModulesDir?: string;
  /** Absolute path to the pre-built theme stylesheet (`theme.css`). */
  themePath?: string;
  /** Pre-built active theme/accent stylesheets, loaded on demand. */
  themeAssetsDir?: string;
  /** Absolute path to the pre-built theme runtime (`theme.js`). */
  themeJsPath?: string;
  /** Resolved theme config: enables `<html data-theme>` + `theme.css` link. */
  theme?: ResolvedTheme;
  /** Absolute path to the pre-built Wire UI stylesheet (`ui.css`). */
  uiCssPath?: string;
  /** Combined production theme + Wire UI stylesheet. */
  frameworkCssPath?: string;
  /** Pre-built `window.__wireSchemas = {...}` script for client validation. */
  schemasJs?: string;
  /**
   * Authorization declarations discovered by `wrnexus build` from
   * `app/authz/*.ts`, statically imported into the generated entry (the
   * catalog holds policy FUNCTIONS, so — unlike `schemasJs` — it cannot be
   * JSON-serialised). `module` is `undefined` for a file with no default
   * export. In the NORMAL generated-entry build, the catalog is already set
   * by the generated `.authz-setup.ts` module before this ever runs (see
   * `applyAuthzManifestEarly` below); `createProductionHandlers` merges this
   * same list again as an idempotent second pass — with its warnings — so a
   * caller that bypasses the generated entry and calls it directly still gets
   * a correctly merged catalog.
   */
  authz?: AuthzManifestEntry[];
  /** Resolved i18n bundle (default lang + locale messages). */
  i18n?: ResolvedI18n;
  /** Default database connection (driver + url); enables `getDb()`. */
  db?: { driver: string; url: string };
  /** Named databases, reached with `getDb("<name>")`. */
  databases?: Record<string, { driver: string; url: string }>;
  /**
   * Absolute path to the default db's migrations bundled into the build
   * (`dist/migrations`). When set, they are applied on startup — like dev.
   */
  migrationsDir?: string;
  /** Bundled migrations dirs for named dbs (name → `dist/db/<name>/migrations`). */
  databaseMigrationDirs?: Record<string, string>;
  /**
   * Auto-apply bundled migrations on server startup (default: true). Set false
   * for deploys that migrate in a separate release step (e.g. multiple instances
   * behind a load balancer, where you migrate once before rolling out).
   */
  autoMigrate?: boolean;
  /** Realtime scaling: bridge room broadcasts over Redis across app processes. */
  realtime?: { scale?: boolean; redisUrl?: string };
  /** File-upload storage: named stores (local dir / S3). Local dirs resolve against cwd. */
  storage?: StorageConfig;
  /** Cache-busting version appended to framework asset URLs. */
  assetVersion?: string;
  /** Package browser runtimes already emitted by the production build. */
  clientRuntimes?: ClientRuntimeDefinition[];
  /** Public URL to emitted package asset metadata. */
  pluginAssets?: Record<string, ProductionPluginAsset>;
  /** Absolute path to copied public assets, if any. */
  publicDir?: string;
  /** Raw HTML appended to every page head. */
  head?: string;
  /** Global SEO defaults. */
  seo?: SeoConfig;
  mobile?: MobileConfig;
  pwa?: PwaConfig | false;
  /** Framework security headers and CORS policy. */
  security?: SecurityConfig;
  /** Built-in request tracing and Server-Timing policy. */
  observability?: ObservabilityConfig;
  /** Built-in tenant identity resolution. */
  tenancy?: TenancyConfig;
  /** Page navigation strategy. */
  navigation?: NavigationConfig;
  port?: number;
  hostname?: string;
  maxBodyBytes?: number;
  /** Keep-alive idle timeout in seconds. Defaults to 30. */
  idleTimeout?: number;
  /** Allow multiple Bun workers to share the listening port. */
  reusePort?: boolean;
  /** Enable only for the CLI's supervised exact-production development mode. */
  developmentRuntime?: boolean;
}

const MODE: Mode = "production";
const JS_HEADERS = {
  "content-type": "text/javascript; charset=utf-8",
  "cache-control": "public, max-age=31536000, immutable",
};
const CSS_HEADERS = {
  "content-type": "text/css; charset=utf-8",
  "cache-control": "public, max-age=31536000, immutable",
};

function resolvePort(explicit?: number): number {
  if (typeof explicit === "number" && Number.isFinite(explicit)) return explicit;

  const envPort = process.env.PORT;
  if (!envPort) return 3000;

  const parsed = Number(envPort);
  return Number.isFinite(parsed) ? parsed : 3000;
}

/** Resolve the production bind address. Gateway-managed app servers override
 * this with loopback so only the gateway is externally reachable. */
export function resolveProductionHostname(
  explicit?: string,
  environmentHostname = process.env.WRNEXUS_HOSTNAME,
): string {
  return environmentHostname?.trim() || explicit || "0.0.0.0";
}

/** One `app/authz/*.ts` declaration as passed through `ProdOptions.authz`. */
export interface AuthzManifestEntry {
  source: string;
  /** Undefined when the declaration file has no default export. */
  module?: AuthzModule;
}

function resolveAuthzSources(
  entries: AuthzManifestEntry[],
): { source: string; module: AuthzModule }[] {
  return entries.flatMap((entry) =>
    entry.module ? [{ source: entry.source, module: entry.module }] : [],
  );
}

/**
 * Merge + `setAuthzCatalog` as EARLY as possible, deliberately silently (no
 * missing-default-export warnings). Called ONLY from the generated
 * `.authz-setup.ts` module that `wrnexus build` imports FIRST in the
 * production entry — before any other static import, including app
 * middleware — so that a middleware module reading `getAuthzCatalog()` at its
 * own module scope (the same eager shape `authzMiddleware({ catalog, ... })`
 * itself requires) sees a populated catalog. `createProductionHandlers` below
 * performs the exact same merge again, WITH its warnings, as the canonical,
 * always-warns second pass — this function stays silent specifically so the
 * normal boot path does not print the same "no default export" warning
 * twice. A genuine conflict still throws here (via `mergeCatalogs`), which
 * fails the boot at import time — before the entry body, and thus
 * `createProductionHandlers`, ever runs.
 */
export function applyAuthzManifestEarly(entries: AuthzManifestEntry[]): void {
  setAuthzCatalog(mergeCatalogs(resolveAuthzSources(entries)));
}

/** Build the route-matching tables + a module map from the manifest. */
function buildProdRouter(manifest: ProdManifest): {
  router: Router;
  modules: Map<string, RouteModule>;
} {
  const modules = new Map<string, RouteModule>();

  const toRoutes = (entries: ManifestRoute[]): Route[] => {
    const routes = entries.map((e): Route => {
      const { regex, paramNames } = compileRoutePattern(e.raw);
      // Use the raw pattern as a stable module key.
      modules.set(
        e.raw,
        e.staticShell === undefined ? e.mod : { ...e.mod, __wrnexusStaticShell: e.staticShell },
      );
      return { raw: e.raw, file: e.raw, regex, paramNames };
    });
    return sortRoutes(routes);
  };

  const pages = toRoutes(manifest.pages);
  const api = toRoutes(manifest.api);
  const realtime = toRoutes(manifest.realtime);

  const optimizedMatcher = (routes: Route[]) => {
    const exact = new Map<string, Route>();
    const dynamic: Route[] = [];
    for (const route of routes) {
      if (route.paramNames.length === 0) exact.set(route.raw, route);
      else dynamic.push(route);
    }
    return (pathname: string) => {
      const route = exact.get(pathname);
      return route ? { route, params: {} } : matchRoute(dynamic, pathname);
    };
  };

  // Components are keyed by name; the runtime resolves them via loadModule(name).
  for (const c of manifest.components) modules.set(c.name, c.mod);
  // Layouts share the module map under a `layout:` prefix (no name collisions).
  for (const l of manifest.layouts) modules.set(`layout:${l.name}`, l.mod);
  for (const service of manifest.services ?? [])
    modules.set(`service:${service.name}`, service.mod);

  const router: Router = {
    pages,
    api,
    realtime,
    middlewareFiles: [],
    components: manifest.components.map((c) => ({ name: c.name, file: c.name })),
    layouts: manifest.layouts.map((l) => ({ name: l.name, file: `layout:${l.name}` })),
    stores: [],
    schemas: [], // descriptors are pre-baked into schemasJs; not needed at runtime
    authz: [], // authz declarations are not needed at runtime in production
    services: (manifest.services ?? []).map((service) => ({
      name: service.name,
      file: `service:${service.name}`,
    })),
    matchPage: optimizedMatcher(pages),
    matchApi: optimizedMatcher(api),
    matchRealtime: optimizedMatcher(realtime),
  };

  return { router, modules };
}

const productionFileCache = new Map<string, ReturnType<typeof Bun.file>>();
const missingProductionFiles = new Set<string>();

/** Serve a pre-built asset file from disk, caching stable build file handles. */
async function serveFile(path: string | undefined, headers: Record<string, string>) {
  if (!path) return new Response("Not Found", { status: 404 });
  if (missingProductionFiles.has(path)) return new Response("Not Found", { status: 404 });
  let file = productionFileCache.get(path);
  if (!file) {
    file = Bun.file(path);
    if (!(await file.exists())) {
      missingProductionFiles.add(path);
      return new Response("Not Found", { status: 404 });
    }
    productionFileCache.set(path, file);
  }
  return new Response(file, { headers });
}

/** Production asset server: pre-built files from disk, reactive runtime inlined. */
function createProdAssetServer(opts: ProdOptions): AssetServer {
  return {
    async serve(pathname: string): Promise<Response | null> {
      if (pathname.startsWith("/__wrnexus/client/")) {
        const name = pathname.slice("/__wrnexus/client/".length);
        if (!opts.clientModulesDir || !/^[A-Za-z0-9._-]+\.mjs$/.test(name)) {
          return new Response("Not Found", { status: 404 });
        }
        return serveFile(join(opts.clientModulesDir, name), JS_HEADERS);
      }
      if (pathname === "/__wrnexus/reactive.js") {
        if (opts.reactivePath) {
          const file = Bun.file(opts.reactivePath);
          if (await file.exists()) return new Response(file, { headers: JS_HEADERS });
        }
        return new Response(getReactiveRuntime(), { headers: JS_HEADERS });
      }
      if (pathname === "/__wrnexus/controllers.js") {
        if (opts.controllersPath) return serveFile(opts.controllersPath, JS_HEADERS);
        return new Response(getComponentControllerRuntime(), { headers: JS_HEADERS });
      }
      if (pathname === "/__wrnexus/nav.js")
        return new Response(getNavRuntime(), { headers: JS_HEADERS });
      if (pathname === "/__wrnexus/realtime.js")
        return new Response(getRealtimeRuntime(), { headers: JS_HEADERS });
      if (pathname === "/__wrnexus/actions.js")
        return new Response(getActionRuntime(), { headers: JS_HEADERS });
      if (pathname === "/__wrnexus/validate.js")
        return new Response(VALIDATE_RUNTIME, { headers: JS_HEADERS });
      if (pathname === "/__wrnexus/i18n.js")
        return new Response(I18N_RUNTIME, { headers: JS_HEADERS });
      if (pathname === UPLOAD_JS_HREF) return new Response(UPLOAD_RUNTIME, { headers: JS_HEADERS });
      if (pathname.startsWith(UPLOADS_PREFIX)) {
        return (await serveStoredFile(pathname)) ?? new Response("Not Found", { status: 404 });
      }
      if (pathname === "/__wrnexus/schemas.js") {
        return new Response(opts.schemasJs ?? "window.__wireSchemas={};", { headers: JS_HEADERS });
      }
      if (pathname === "/__wrnexus/theme.css") return serveFile(opts.themePath, CSS_HEADERS);
      const activeThemeMatch = /^\/__wrnexus\/theme\/([^/]+)\/([^/]+)\.css$/.exec(pathname);
      if (activeThemeMatch && opts.theme && opts.themeAssetsDir) {
        try {
          const themeName = decodeURIComponent(activeThemeMatch[1]!);
          const accentPart = decodeURIComponent(activeThemeMatch[2]!);
          if (!opts.theme.names.includes(themeName))
            return new Response("Not Found", { status: 404 });
          if (accentPart !== "_" && !opts.theme.accentNames.some((name) => name === accentPart)) {
            return new Response("Not Found", { status: 404 });
          }
          return serveFile(
            join(
              opts.themeAssetsDir,
              encodeURIComponent(themeName),
              `${accentPart === "_" ? "_" : encodeURIComponent(accentPart)}.css`,
            ),
            CSS_HEADERS,
          );
        } catch {
          return new Response("Not Found", { status: 404 });
        }
      }
      if (pathname === "/__wrnexus/theme.js") return serveFile(opts.themeJsPath, JS_HEADERS);
      if (pathname === "/__wrnexus/ui.css") return serveFile(opts.uiCssPath, CSS_HEADERS);
      if (pathname === "/__wrnexus/framework.css")
        return serveFile(opts.frameworkCssPath, CSS_HEADERS);
      if (pathname === "/__wrnexus/styles.css") return serveFile(opts.stylesPath, CSS_HEADERS);
      const pluginAsset = opts.pluginAssets?.[pathname];
      if (pluginAsset) {
        return serveFile(pluginAsset.path, {
          "content-type": pluginAsset.contentType,
          "cache-control":
            pluginAsset.immutable === false ? "no-cache" : "public, max-age=31536000, immutable",
          "x-content-type-options": "nosniff",
        });
      }
      return servePublicAsset(opts.publicDir, pathname, MODE);
    },
  };
}

/**
 * Build the portable request handler from a precompiled manifest — a
 * WinterCG-style `fetch(request) => Response` plus the websocket handlers, with
 * NO server bound. This is the deployment-adapter seam: `createProductionServer`
 * wraps it in `Bun.serve`, `serveNode` bridges it onto `node:http`, and edge or
 * serverless targets can call `fetch` directly.
 */
export function createProductionHandlers(
  manifest: ProdManifest,
  opts: ProdOptions,
): ReturnType<typeof createHandlers> {
  const { router, modules } = buildProdRouter(manifest);
  const assets = createProdAssetServer(opts);

  // Configure the default + named databases. Migrations must already be applied
  // (`wrnexus db migrate [--db=<name>]` against the production databases).
  if (opts.db) {
    try {
      setDb(connectFromConfig(opts.db));
    } catch (err) {
      console.warn("[wrnexus] database setup failed:", err instanceof Error ? err.message : err);
    }
  }
  for (const [name, cfg] of Object.entries(opts.databases ?? {})) {
    registerLazyDb(name, () => connectFromConfig(cfg));
  }

  // File-upload storage. Relative local dirs resolve against the deployment cwd
  // (NOT dist/, which is rebuilt) so uploads persist across deploys.
  configureStorage(opts.storage, process.cwd());

  // Authorization: merge the build's statically-imported app/authz/*.ts
  // declarations into the process-wide catalog BEFORE the handlers (and thus
  // any request) exist, so a conflicting pair of declarations fails the boot
  // loudly instead of surfacing on the first request. This runs for every
  // deployment adapter that calls createProductionHandlers, not only the
  // Bun.serve path in createProductionServer below. The app still registers
  // authzMiddleware itself with its own store; this only makes the merged
  // catalog reachable. No declarations -> an empty catalog, no error.
  //
  // In the NORMAL generated-entry build, this is a deliberately redundant
  // SECOND pass: the generated `.authz-setup.ts` module already ran this
  // exact merge (silently, via applyAuthzManifestEarly above) before this
  // function was ever called, specifically so a middleware module that reads
  // getAuthzCatalog() at its own module scope sees a populated catalog — this
  // function's body runs too late for that (it is reached only once every
  // OTHER static import, including middleware, has already evaluated).
  //
  // The merge+validation of opts.authz always runs (a genuine conflict must
  // still fail the boot loudly, no matter which pass discovers it). But
  // setAuthzCatalog is only called when this pass actually has something to
  // contribute, OR when nothing has been set yet: client.ts documents an
  // escape hatch where a direct caller of createProductionHandlers may call
  // setAuthzCatalog(catalog) itself before importing anything that reads it,
  // specifically for a custom entry that never ran the generated
  // `.authz-setup.ts` pass. Calling setAuthzCatalog unconditionally here would
  // clobber that caller's catalog with an empty one whenever opts.authz is
  // omitted — silently deleting every permission the app declared.
  for (const missing of (opts.authz ?? []).filter((entry) => !entry.module)) {
    console.warn(`[wrnexus] authz declaration ${missing.source} has no default export; skipping.`);
  }
  const mergedAuthzCatalog = mergeCatalogs(resolveAuthzSources(opts.authz ?? []));
  if ((opts.authz?.length ?? 0) > 0 || !hasAuthzCatalog()) {
    setAuthzCatalog(mergedAuthzCatalog);
  }

  // Middleware is already an ordered array of functions.
  const getMiddleware = async (): Promise<Middleware[]> => manifest.middleware;

  // In prod, modules are pre-imported; "loading" is a map lookup.
  const loadModule = async (key: string): Promise<RouteModule> => {
    const mod = modules.get(key);
    if (!mod) throw new Error(`No module registered for route ${key}`);
    return mod;
  };

  const productionHmr = opts.developmentRuntime === true;
  const handlers = createHandlers({
    mode: MODE,
    hmr: productionHmr,
    hub: productionHmr ? new HmrHub() : undefined,
    router,
    loadModule,
    getMiddleware,
    assets,
    hasStyles: !!opts.stylesPath,
    hasUi: !!opts.uiCssPath,
    hasFrameworkStyles: !!opts.frameworkCssPath,
    theme: opts.theme,
    i18n: opts.i18n,
    inlineStyles: opts.inlineStyles,
    stylesIncludeFramework: opts.stylesIncludeFramework,
    stylesIncludeUi: opts.stylesIncludeUi,
    assetVersion: opts.assetVersion,
    clientRuntimes: opts.clientRuntimes,
    head: opts.head,
    seo: opts.seo,
    mobile: opts.mobile,
    pwa: opts.pwa,
    security: opts.security,
    observability: opts.observability,
    tenancy: opts.tenancy,
    navigation: opts.navigation,
    maxBodyBytes: opts.maxBodyBytes,
    realtimeBus: realtimeBusFromConfig(opts.realtime),
  });

  return handlers;
}

/**
 * Apply migrations bundled into the build before the server accepts traffic, so
 * a fresh deploy always runs on the latest schema — exactly like the dev server
 * auto-migrates on startup. Applied migrations are tracked in `_wire_migrations`,
 * so this is idempotent and safe to run on every boot. Opt out with
 * `autoMigrate: false` (e.g. multi-instance deploys that migrate in a release
 * step). A failed migration is logged but does not crash the server: each
 * migration runs in a transaction, so the DB is left at the last good state.
 */
async function runStartupMigrations(opts: ProdOptions): Promise<void> {
  if (opts.autoMigrate === false) return;

  const targets: { name?: string; dir: string }[] = [];
  if (opts.db && opts.migrationsDir) targets.push({ dir: opts.migrationsDir });
  for (const [name, dir] of Object.entries(opts.databaseMigrationDirs ?? {})) {
    targets.push({ name, dir });
  }

  for (const { name, dir } of targets) {
    const label = name ? ` (db: ${name})` : "";
    if (!hasDb(name)) continue;
    try {
      const applied = await migrate(getDb(name), dir);
      if (applied.length) {
        console.log(
          `WrNexus: applied ${applied.length} migration(s)${label}: ${applied.join(", ")}`,
        );
      }
    } catch (err) {
      console.error(
        `WrNexus: migration failed${label} —`,
        err instanceof Error ? err.message : err,
      );
    }
  }
}

/** Start the production server on Bun from a precompiled manifest. */
export async function createProductionServer(manifest: ProdManifest, opts: ProdOptions) {
  // Load the deployment's .env cascade for the active profile (real env wins),
  // so runtime secrets are available even though config was baked at build time.
  loadEnv(process.cwd(), resolveProfile({ mode: "production" }));

  const handlers = createProductionHandlers(manifest, opts);

  // Bring the schema up to date before listening (opt out with autoMigrate:false).
  await runStartupMigrations(opts);

  const server = Bun.serve<WsData>({
    port: resolvePort(opts.port),
    hostname: resolveProductionHostname(opts.hostname),
    development: false,
    maxRequestBodySize: opts.maxBodyBytes ?? 10 * 1024 * 1024,
    idleTimeout: opts.idleTimeout ?? 30,
    reusePort: opts.reusePort ?? false,
    fetch: handlers.fetch,
    websocket: handlers.websocket,
  });

  // Graceful shutdown: stop accepting connections, then exit.
  let shuttingDown = false;
  const shutdown = () => {
    if (shuttingDown) return;
    shuttingDown = true;
    console.log("WrNexus: shutting down…");
    server.stop();
    process.exit(0);
  };
  process.on("SIGTERM", shutdown);
  process.on("SIGINT", shutdown);

  console.log(`WrNexus (production) listening on http://${server.hostname}:${server.port}`);
  return server;
}
