/**
 * @wrnexus/dev-server — the development HTTP + WebSocket server.
 *
 * Thin Bun.serve wrapper around the shared runtime (runtime.ts). Dynamic module
 * loading and targeted cache invalidation keep page/component/API edits inside
 * the running process while the HMR socket morphs fresh HTML into the browser.
 */

import { resolve, dirname, join } from "node:path";
import type { Middleware, Mode, SecurityConfig, SeoConfig } from "@wrnexus/core";
import { buildRouter, type Router } from "@wrnexus/router";
import {
  resolveThemeConfig,
  type StylesConfig,
  type ThemeConfig,
  type MobileConfig,
  type PwaConfig,
} from "@wrnexus/styles";
import { uiComponentsDir, uiCss } from "@wrnexus/ui";
import { renderSchemasScript, type ObjectSchema, type SchemaDescriptor } from "@wrnexus/validation";
import { loadLocales, resolveI18n, type I18nConfig } from "@wrnexus/i18n";
import { migrate, setDb, registerDb } from "@wrnexus/db";
import { connectFromConfig } from "@wrnexus/db/connect";
import { configureStorage, type StorageConfig } from "@wrnexus/uploader";
import { realtimeBusFromConfig } from "./realtime-bus.ts";
import { invalidateModule, loadModule, setCompileCacheDir } from "./pipeline.ts";
import { createHandlers, type WsData } from "./runtime.ts";
import { createDevAssetServer } from "./assets.ts";
import { HmrHub } from "./hmr.ts";
import { startWatcher } from "./watch.ts";
export { RESTART_EXIT_CODE } from "./restart.ts";

export interface ServeOptions {
  appDir: string;
  port?: number;
  hostname?: string;
  mode?: Mode;
  /** Inject the live-reload client (defaults to true in development). */
  hmr?: boolean;
  /** Resolved absolute path to the global CSS entry, or null. */
  styleEntry?: string | null;
  /** Custom styles config (e.g. a Tailwind/PostCSS processor). */
  stylesConfig?: StylesConfig;
  /** Raw HTML appended to every page head (from wrnexus.config.ts). */
  head?: string;
  /** Global SEO defaults. */
  seo?: SeoConfig;
  /** Framework security headers and CORS policy. */
  security?: SecurityConfig;
  /** Design-token theme config (merged over the built-in light/dark). */
  theme?: ThemeConfig;
  /** i18n config (default language + supported locales). */
  i18n?: I18nConfig;
  /** Default database connection (driver + url). Enables `getDb()` and dev auto-migrate. */
  db?: { driver: string; url: string };
  /** Named databases, reached with `getDb("<name>")`; migrations under app/db/<name>/. */
  databases?: Record<string, { driver: string; url: string }>;
  /** Realtime scaling: bridge room broadcasts over Redis across app processes. */
  realtime?: { scale?: boolean; redisUrl?: string };
  /** File-upload storage: named stores (local dir / S3), reached with `getStore()`. */
  storage?: StorageConfig;
  mobile?: MobileConfig;
  pwa?: PwaConfig | false;
}

export interface RunningServer {
  port: number;
  hostname: string;
  url: string;
  router: Router;
  stop(): void;
}

/** Build an invalidatable middleware loader for in-process hot updates. */
function middlewareLoader(router: Router): {
  load: () => Promise<Middleware[]>;
  invalidate: () => void;
} {
  let cache: Middleware[] | null = null;
  return {
    async load() {
      if (cache) return cache;
      const out: Middleware[] = [];
      for (const file of router.middlewareFiles) {
        const mod = await loadModule(file);
        if (typeof mod.default === "function") out.push(mod.default as Middleware);
        else console.warn(`[wrnexus] middleware ${file} has no default export; skipped`);
      }
      cache = out;
      return out;
    },
    invalidate() {
      cache = null;
    },
  };
}

async function schemaRuntime(router: Router): Promise<string> {
  const descriptors: Record<string, SchemaDescriptor> = {};
  for (const schemaRef of router.schemas) {
    try {
      const mod = await loadModule(schemaRef.file);
      const schema = mod.default as ObjectSchema | undefined;
      if (schema && typeof schema.describe === "function") {
        descriptors[schemaRef.name] = schema.describe();
      }
    } catch (error) {
      console.warn(`[wrnexus] schema '${schemaRef.name}' failed to load`, error);
    }
  }
  return renderSchemasScript(descriptors);
}

export async function startServer(opts: ServeOptions): Promise<RunningServer> {
  const appDir = resolve(opts.appDir);
  const mode: Mode = opts.mode ?? "development";
  const hmr = opts.hmr ?? mode === "development";
  const port = opts.port ?? 3000;
  const hostname = opts.hostname ?? "::";
  const displayHost = hostname === "0.0.0.0" || hostname === "::" ? "localhost" : hostname;

  const router = buildRouter(appDir, { componentDirs: [uiComponentsDir()] });
  const styleEntry = opts.styleEntry ?? null;
  const appRoot = dirname(appDir);
  // Compile every `.wrn` into ONE cache dir at the project root, instead of a
  // `.wrnexus/` next to each source file (and inside node_modules UI dirs).
  setCompileCacheDir(join(appRoot, ".wrnexus"));
  const theme = resolveThemeConfig(opts.theme);
  const uiStyles = uiCss();

  const schemasJs = await schemaRuntime(router);

  // i18n is opt-in by the presence of app/locales/*.json.
  const localeMessages = loadLocales(join(appDir, "locales"));
  const i18n = Object.keys(localeMessages).length
    ? resolveI18n(localeMessages, opts.i18n)
    : undefined;

  // Databases: configure the default (getDb()) + each named one (getDb("<name>")),
  // and auto-migrate in dev so schemas are ready. The default's migrations live in
  // app/db/migrations; a named db's in app/db/<name>/migrations. Prod runs
  // migrations explicitly (files aren't in the bundle).
  const connectAndMigrate = async (name: string | null, cfg: { driver: string; url: string }) => {
    try {
      const db = name
        ? registerDb(name, connectFromConfig(cfg, appRoot))
        : setDb(connectFromConfig(cfg, appRoot));
      const dir = name ? join(appDir, "db", name, "migrations") : join(appDir, "db", "migrations");
      const applied = await migrate(db, dir);
      if (applied.length) {
        console.log(
          `[wrnexus] applied ${applied.length} migration(s)${name ? ` to '${name}'` : ""}`,
        );
      }
    } catch (err) {
      const label = name ? `database '${name}'` : "database";
      console.warn(`[wrnexus] ${label} setup failed:`, err instanceof Error ? err.message : err);
    }
  };
  if (opts.db) await connectAndMigrate(null, opts.db);
  for (const [name, cfg] of Object.entries(opts.databases ?? {}))
    await connectAndMigrate(name, cfg);

  // File-upload storage: build a driver per configured store (local dir / S3).
  // Relative local dirs resolve against the app root; served/served-back below.
  configureStorage(opts.storage, appRoot);

  const assets = createDevAssetServer(
    appDir,
    mode,
    {
      entry: styleEntry,
      config: opts.stylesConfig,
      appRoot,
      publicDir: join(appRoot, "public"),
    },
    theme,
    uiStyles,
    schemasJs,
  );

  const hub = hmr ? new HmrHub() : undefined;
  const middleware = middlewareLoader(router);

  const runtimeDeps = {
    mode,
    hmr,
    router,
    loadModule,
    getMiddleware: middleware.load,
    assets,
    hasStyles: !!styleEntry,
    hasUi: true,
    theme,
    i18n,
    head: opts.head,
    seo: opts.seo,
    mobile: opts.mobile,
    pwa: opts.pwa,
    security: opts.security,
    hub,
    realtimeBus: realtimeBusFromConfig(opts.realtime),
  };
  const handlers = createHandlers(runtimeDeps);

  const server = Bun.serve<WsData>({
    port,
    hostname,
    development: mode === "development",
    maxRequestBodySize: 10 * 1024 * 1024,
    fetch: handlers.fetch,
    websocket: handlers.websocket,
  });

  let watcher: ReturnType<typeof startWatcher>;

  // In-process HMR: keep the server and socket alive, invalidate only changed
  // modules, rescan file routes, and ask browsers to morph in fresh HTML.
  if (hmr && hub) {
    const hotUpdate = async (files: string[]): Promise<void> => {
      // Allow VS Code/Bun to finish writing pasted content.
      await new Promise((resolvePromise) => setTimeout(resolvePromise, 50));

      for (const relative of files) {
        invalidateModule(resolve(appDir, relative));
      }

      Object.assign(
        router,
        buildRouter(appDir, {
          componentDirs: [uiComponentsDir()],
        }),
      );
      middleware.invalidate();

      if (files.some((file) => file === "schemas" || file.startsWith("schemas/"))) {
        assets.updateSchemas(await schemaRuntime(router));
      }
      if (files.some((file) => file === "locales" || file.startsWith("locales/"))) {
        const messages = loadLocales(join(appDir, "locales"));
        runtimeDeps.i18n = Object.keys(messages).length
          ? resolveI18n(messages, opts.i18n)
          : undefined;
      }

      console.log(`[wrnexus] hot update — ${files.join(", ")}`);
      hub.reload();
    };
    watcher = startWatcher({ appDir, hub, assets, onHotChange: hotUpdate });
  }

  const boundPort = server.port ?? port;
  return {
    port: boundPort,
    hostname,
    url: `http://${displayHost}:${boundPort}`,
    router,
    stop: () => {
      watcher?.close();
      server.stop();
    },
  };
}

export { createHandlers } from "./runtime.ts";
export type { RuntimeDeps, AssetServer, WsData } from "./runtime.ts";

// Multi-app gateway: route multiple apps by domain behind one port.
export { startGateway } from "./gateway.ts";
export type {
  GatewayApp,
  GatewayOptions,
  GatewayAuth,
  GatewaySecurity,
  RunningGateway,
} from "./gateway.ts";

// Deployment: the portable production handler + the node:http adapter.
export { createProductionServer, createProductionHandlers } from "./prod.ts";
export { toRequest, writeResponse, nodeListener, serveNode } from "./adapters/node.ts";
export type { FetchHandler } from "./adapters/node.ts";
