/**
 * @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 { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { resolve, dirname, isAbsolute, 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,
  type NavigationConfig,
} from "@wrnexus/styles";
import { uiComponentsDir, uiCssPath } from "@wrnexus/ui/registry";
import { renderSchemasScript, type ObjectSchema, type SchemaDescriptor } from "@wrnexus/validation";
import { loadLocales, resolveI18n, type I18nConfig } from "@wrnexus/i18n";
import {
  applyMigrations,
  loadMigrations,
  migrate,
  setDb,
  registerDb,
  registerLazyDb,
  getDbPerformanceSnapshot,
} from "@wrnexus/db";
import { connectFromConfig } from "@wrnexus/db/connect";
import { configureStorage, type StorageConfig } from "@wrnexus/uploader";
import { setAuthzCatalog, type AuthzCatalog, type AuthzModule } from "@wrnexus/authz";
import { loadAppAuthzCatalog } from "./authz-boot.ts";
import { realtimeBusFromConfig } from "./realtime-bus.ts";
import {
  invalidateModule,
  loadModule,
  loadWrnServerModule,
  setCompileCacheDir,
  setCompileImportOptions,
  setDevCompilerPipeline,
  wrnBrowserArtifactUrlAsync,
} from "./pipeline.ts";
import { createRpcHandler } from "@wrnexus/ssr/rpc";
import { createHandlers, type WsData } from "./runtime.ts";
import { createDevAssetServer } from "./assets.ts";
import { pluginAssetsFromContributions } from "./plugin-assets.ts";
import { resolvePackageMigrations } from "./plugin-migrations.ts";
import { HmrHub } from "./hmr.ts";
import { startWatcher } from "./watch.ts";
export { RESTART_EXIT_CODE } from "./restart.ts";
export { expandStaticComponents, precomputePartialStaticShell } from "./partial-build.ts";
export { getWrnCompileMetrics, resetWrnCompileMetrics } from "./pipeline.ts";
export type { WrnCompileMetrics } from "./pipeline.ts";
import { resetDevCache } from "./cache.ts";

import type { DevToolbarConfig } from "@wrnexus/dev-toolbar/types";
import { createPluginRunner, discoverPlugins, type PluginInput } from "@wrnexus/plugin";
import type { ObservabilityConfig, TenancyConfig } from "@wrnexus/styles";

import {
  builtinDevToolbarPanels,
  createDevToolbarCollector,
  type DevToolbarCollector,
} from "@wrnexus/dev-toolbar/server";

export interface ServeOptions {
  appDir: string;
  port?: number;
  hostname?: string;
  /** Development TLS material. Production TLS is normally terminated by the deployment proxy. */
  tls?: { cert: string; key: string };
  mode?: Mode;
  /** Inject the live-reload client (defaults to true in development). */
  hmr?: boolean;
  appConfig?: Record<string, unknown>;
  /** 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;
  devToolbar?: boolean | DevToolbarConfig;
  plugins?: PluginInput;
  observability?: ObservabilityConfig;
  tenancy?: TenancyConfig;
  navigation?: NavigationConfig;
}

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

function resolveDevToolbarConfig(
  mode: string,
  value: ServeOptions["devToolbar"],
): DevToolbarConfig | null {
  if (mode !== "development" || value === false || value === undefined) {
    return null;
  }

  if (value === true) {
    return {
      enabled: true,
      position: "bottom-center",
      defaultOpen: false,
      scanOnNavigation: true,
      scanOnHmr: true,
      openEditor: true,
    };
  }

  if (value.enabled === false) {
    return null;
  }

  return {
    enabled: true,
    position: "bottom-center",
    defaultOpen: false,
    scanOnNavigation: true,
    scanOnHmr: true,
    openEditor: true,
    ...value,
  };
}

export async function startServer(opts: ServeOptions): Promise<RunningServer> {
  const appDir = resolve(opts.appDir);
  const appRoot = dirname(appDir);
  const mode: Mode = opts.mode ?? "development";
  const importConfig = (opts.appConfig?.imports ?? {}) as {
    mode?: "legacy" | "compatible" | "explicit";
    autoImport?: boolean;
    aliases?: Record<string, string>;
  };
  setCompileImportOptions(appRoot, importConfig);
  const configuredPlugins = opts.plugins ?? (opts.appConfig?.plugins as PluginInput | undefined);

  const discoveredPlugins = await discoverPlugins(appRoot, configuredPlugins, {
    includeDevDependencies: true,
    strict: true,
    warn: (message) => console.warn(`[wrnexus:plugin] ${message}`),
    enforcePermissions: (opts.appConfig?.pluginPermissions as { enforce?: boolean } | undefined)
      ?.enforce,
    grantedPermissions: (
      opts.appConfig?.pluginPermissions as
        { grants?: Record<string, import("@wrnexus/plugin").PluginPermission[]> } | undefined
    )?.grants,
  });

  const pluginRunner = createPluginRunner(discoveredPlugins, {
    root: appRoot,
    mode,
    command: "dev",
    metadata: new Map(),
    warn: (message) => console.warn(`[wrnexus:plugin] ${message}`),
  });

  /**
   * Plugins must receive the complete
   * wrnexus.config.ts object.
   *
   * ServeOptions only contains framework-known
   * properties. Package-specific configuration,
   * such as `auth`, is stored in appConfig.
   */
  const pluginConfig: Record<string, unknown> = {
    ...(opts.appConfig ?? {}),

    // Resolved runtime values take priority.
    appDir,
    port: opts.port,
    hostname: opts.hostname,
    mode,
    hmr: opts.hmr,

    styleEntry: opts.styleEntry,

    styles: opts.stylesConfig ?? (opts.appConfig?.styles as StylesConfig | undefined),

    head: opts.head,
    seo: opts.seo,
    security: opts.security,
    theme: opts.theme,
    i18n: opts.i18n,
    db: opts.db,
    databases: opts.databases,
    realtime: opts.realtime,
    storage: opts.storage,
    mobile: opts.mobile,
    pwa: opts.pwa,
    devToolbar: opts.devToolbar,
    plugins: configuredPlugins,
    observability: opts.observability,
    tenancy: opts.tenancy,
    navigation: opts.navigation,
  };

  await pluginRunner.configure(pluginConfig);

  await pluginRunner.configResolved(
    Object.freeze({
      ...pluginConfig,
    }),
  );

  const pluginContributions = await pluginRunner.contributions();
  const virtualModules = new Map<string, string>();
  // A live dev server and a test process can share an app root. Keeping the
  // generated artifacts in a process-specific directory prevents a test boot
  // from deleting the running server's client modules.
  const cacheDir = join(appRoot, `.wrnexus-${process.pid}`);
  const virtualDir = join(cacheDir, "virtual");
  mkdirSync(virtualDir, { recursive: true });
  for (const [index, module] of pluginContributions.virtualModules.entries()) {
    const output = join(virtualDir, `plugin-${index}.ts`);
    writeFileSync(
      output,
      await module.load({
        root: appRoot,
        mode,
        command: "dev",
        profile: process.env.WRNEXUS_PROFILE,
        metadata: new Map(),
        warn: (message) => console.warn(`[wrnexus:plugin] ${message}`),
      }),
      "utf8",
    );
    virtualModules.set(module.id, output);
  }
  setDevCompilerPipeline({
    transformAst: (ast, file) => pluginRunner.transformAst(ast, file),
    transformCode: (code, file) => pluginRunner.transformCode(code, file),
    virtualModules,
  });

  console.log(
    `[wrnexus:plugin] discovered: ${
      pluginRunner.plugins.map((plugin) => plugin.name).join(", ") || "none"
    }`,
  );

  console.log(`[wrnexus:plugin] contributed routes: ${pluginContributions.routes.length}`);

  const pluginToolbarPanels = await pluginRunner.devToolbarPanels();
  const bundledUiDir = uiComponentsDir();
  const componentDirs = [
    bundledUiDir,
    join(bundledUiDir, "..", "styles"),
    ...pluginContributions.componentDirs,
  ];

  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,
    externalRoutes: pluginContributions.routes,
    middlewareFiles: pluginContributions.middleware,
  });
  const styleEntry = opts.styleEntry ?? null;

  const devToolbarConfig = resolveDevToolbarConfig(mode, opts.devToolbar);

  const devToolbarCollector: DevToolbarCollector | undefined = devToolbarConfig
    ? createDevToolbarCollector()
    : undefined;

  resetDevCache({
    rootDir: appRoot,
    cacheDir,
    enabled: process.env.WRNEXUS_PRESERVE_CACHE !== "1",
  });

  // 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(cacheDir);
  const theme = resolveThemeConfig(opts.theme);
  const uiStylesPath = uiCssPath();

  const schemasJs = await schemaRuntime(router);

  // Authorization: load and merge every app/authz/*.ts declaration, then stash
  // it in the process-wide registry BEFORE middleware is resolved. App
  // middleware (which registers authzMiddleware itself, with its own store —
  // the framework never installs one) runs at request time and needs
  // getAuthzCatalog() already populated by then. An app with no declarations
  // gets an empty catalog; a genuine conflict between declarations throws and
  // fails this boot loudly. Pass the already-built `router` (not `appDir`):
  // it was just built above with the full componentDirs/externalRoutes/
  // middlewareFiles options, so this avoids a second, redundant filesystem
  // scan of the whole app/ tree on every dev boot.
  const authzCatalog: AuthzCatalog = await loadAppAuthzCatalog(router);
  setAuthzCatalog(authzCatalog);

  // i18n is opt-in by the presence of app/locales/*.json.
  const localeMessages = loadLocales(join(appDir, "locales"), { strict: opts.i18n?.strict });
  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 appApplied = await migrate(db, dir);
      const packageApplied = await applyMigrations(
        db,
        resolvePackageMigrations(pluginContributions.migrations, name ?? undefined),
      );
      const applied = [...appApplied, ...packageApplied];
      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 ?? {})) {
    const migrationDir = join(appDir, "db", name, "migrations");
    const hasMigrations =
      loadMigrations(migrationDir).length > 0 ||
      resolvePackageMigrations(pluginContributions.migrations, name).length > 0;
    if (hasMigrations) await connectAndMigrate(name, cfg);
    else registerLazyDb(name, () => connectFromConfig(cfg, appRoot));
  }

  // 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"),
      sources: [
        ...componentDirs,
        ...pluginContributions.styles.flatMap((style) => (style.source ? [style.source] : [])),
      ],
      entries: pluginContributions.styles.flatMap((style) => (style.entry ? [style.entry] : [])),
    },
    theme,
    () => readFileSync(uiStylesPath, "utf8"),
    schemasJs,
    pluginAssetsFromContributions(pluginContributions),
  );

  const hub = hmr ? new HmrHub() : undefined;
  const unsubscribeDevToolbar =
    devToolbarCollector && hub
      ? devToolbarCollector.subscribe((issues) => {
          hub.broadcastJson({ channel: "toolbar", type: "toolbar:issues", issues });
        })
      : undefined;
  const middleware = middlewareLoader(router);
  const toolbarPlatform = {
    wrnexus060: {
      clientFunctions: true,
      serverFunctions: true,
      sharedFunctions: true,
      typedOutputs: true,
      requestScopedStores: true,
    },
    plugins: pluginRunner.plugins.map((plugin) => ({ name: plugin.name, version: plugin.version })),
    runtimes: pluginContributions.clientRuntimes.map((runtime) => ({
      id: runtime.id,
      publicPath: runtime.publicPath,
      type: runtime.type,
      load: runtime.load,
    })),
    assets: pluginContributions.assets.map((asset) => ({
      id: asset.id,
      publicPath: asset.publicPath,
      contentType: asset.contentType,
    })),
    componentDirs,
    styles: pluginContributions.styles,
    routes: {
      pages: router.pages.length,
      api: router.api.length,
      realtime: router.realtime.length,
    },
  };

  const runtimeDeps = {
    mode,
    hmr,
    router,
    loadModule,
    getMiddleware: middleware.load,
    assets,
    hasStyles: !!styleEntry || pluginContributions.styles.some((style) => !!style.entry),
    hasUi: true,
    theme,
    i18n,
    head: opts.head,
    seo: opts.seo,
    mobile: opts.mobile,
    pwa: opts.pwa,
    security: opts.security,
    observability: opts.observability,
    tenancy: opts.tenancy,
    navigation: opts.navigation,
    clientRuntimes: pluginContributions.clientRuntimes,
    hub,
    realtimeBus: realtimeBusFromConfig(opts.realtime),
    renderHtml: (html: string) => pluginRunner.render(html),
    devToolbar:
      devToolbarConfig && devToolbarCollector
        ? {
            config: devToolbarConfig,
            collector: devToolbarCollector,
            root: appRoot,
            panels: () => [
              {
                id: "runtime",
                title: "Runtime",
                icon: "cpu",
                description:
                  "Client, server, and shared functions, hydration modules, and runtime-boundary diagnostics.",
                order: 10,
              },
              {
                id: "stores",
                title: "Stores",
                icon: "database",
                description:
                  "Global/page stores, safe client state, computed values, actions, persistence, and hydration.",
                order: 20,
              },
              {
                id: "cache",
                title: "Cache",
                icon: "layers",
                description:
                  "Request, data, component, and page cache entries plus hit/miss history.",
                order: 30,
              },
              ...builtinDevToolbarPanels({
                root: appRoot,
                platform: toolbarPlatform,
                database: getDbPerformanceSnapshot(),
                version: { current: "0.8.0" },
              }),
              ...pluginToolbarPanels,
            ],
            platform: toolbarPlatform,
          }
        : undefined,
  };
  const handlers = createHandlers(runtimeDeps);
  const rpcHandler = createRpcHandler({
    async resolve(rawComponent) {
      const componentName = rawComponent.split(":", 1)[0] ?? rawComponent;
      const preferred = router.components.find(
        (entry) => entry.name.toLowerCase() === componentName.toLowerCase(),
      );
      const candidates = [
        ...(preferred ? [preferred.file] : []),
        ...router.pages.filter((route) => route.file.endsWith(".wrn")).map((route) => route.file),
        ...router.layouts.map((entry) => entry.file),
        ...router.stores.map((entry) => entry.file),
        ...router.components.filter((entry) => entry !== preferred).map((entry) => entry.file),
      ];
      for (const file of new Set(candidates)) {
        const module = await loadWrnServerModule(file);
        const functions = module.__wrnexusServerFunctions;
        const manifest = module.__wrnexusRpcManifest;
        if (!functions || typeof functions !== "object" || !Array.isArray(manifest)) continue;
        const ownsComponent = manifest.some(
          (entry: any) =>
            String(entry?.component ?? "").toLowerCase() === componentName.toLowerCase(),
        );
        if (!ownsComponent) continue;
        return {
          functions: functions as Record<string, (...args: any[]) => any>,
          manifest: manifest as any,
        };
      }
      return null;
    },
    validateCsrf(request) {
      const url = new URL(request.url);
      const origin = request.headers.get("origin");
      if (origin && origin !== url.origin) return false;
      const cookieHeader = request.headers.get("cookie") ?? "";
      const cookieToken =
        /(?:^|;\s*)wire-csrf=([^;]+)/.exec(cookieHeader)?.[1] ??
        /(?:^|;\s*)wrnexus_csrf=([^;]+)/.exec(cookieHeader)?.[1];
      const headerToken =
        request.headers.get("x-csrf-token") ?? request.headers.get("x-wrnexus-csrf") ?? "";
      return !cookieToken || decodeURIComponent(cookieToken) === headerToken;
    },
  });

  const server = Bun.serve<WsData>({
    port,
    hostname,
    development: mode === "development",
    maxRequestBodySize: 10 * 1024 * 1024,
    ...(opts.tls ? { tls: opts.tls } : {}),
    fetch(request, server) {
      if (new URL(request.url).pathname === "/__wrnexus/rpc") return rpcHandler(request);
      return handlers.fetch(request, server);
    },
    websocket: handlers.websocket,
  });

  try {
    await pluginRunner.hook("configureServer", {
      server,
      router,
      handlers,
      assets,
      devToolbarCollector,
      pluginContributions,
    });
  } catch (error) {
    server.stop();
    throw error;
  }

  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 file of files) {
        invalidateModule(isAbsolute(file) ? file : resolve(appDir, file));
      }
      if (files.some((file) => file.endsWith(".wrn"))) assets.invalidateCss();

      Object.assign(
        router,
        buildRouter(appDir, {
          componentDirs,
          externalRoutes: pluginContributions.routes,
          middlewareFiles: pluginContributions.middleware,
        }),
      );
      middleware.invalidate();

      const appFiles = files.filter((file) => !isAbsolute(file));
      // Without this branch, editing app/authz/*.ts reloaded the page (watch.ts
      // classifies any non-CSS change as "server") while the OLD catalog stayed
      // authoritative — a false security signal: tightening or removing a
      // permission LOOKS like it took effect but does not until a restart. A
      // raw `import()` here would silently no-op: Bun caches local TS/JS
      // modules by filesystem path and ignores query strings, so the edited
      // file must be re-imported through `loadModule` (pipeline.ts), which
      // copies it to a versioned sibling path specifically to defeat that
      // cache — the same mechanism every other hot-reloaded module already
      // uses. `router` was just rebuilt above, so this reuses it rather than
      // re-scanning the filesystem a third time.
      if (appFiles.some((file) => file === "authz" || file.startsWith("authz/"))) {
        try {
          const nextAuthzCatalog = await loadAppAuthzCatalog(
            router,
            (file) => loadModule(file) as Promise<{ default?: AuthzModule }>,
          );
          setAuthzCatalog(nextAuthzCatalog);
        } catch (error) {
          console.error(
            "[wrnexus] authz hot update failed — the PREVIOUS catalog remains authoritative " +
              "until this is fixed and the file saved again",
            error,
          );
        }
      }
      if (appFiles.some((file) => file === "schemas" || file.startsWith("schemas/"))) {
        assets.updateSchemas(await schemaRuntime(router));
      }
      if (appFiles.some((file) => file === "locales" || file.startsWith("locales/"))) {
        try {
          const messages = loadLocales(join(appDir, "locales"), { strict: opts.i18n?.strict });
          runtimeDeps.i18n = Object.keys(messages).length
            ? resolveI18n(messages, opts.i18n)
            : undefined;
        } catch (error) {
          console.warn(
            "[wrnexus] locale hot update was incomplete; keeping the last valid bundle",
            error,
          );
        }
      }

      console.log(`[wrnexus] hot update — ${files.join(", ")}`);
      await pluginRunner.hook("hmrUpdate", files);
      const storeUpdates: Array<{ name: string; url: string; kind: string }> = [];
      for (const changed of files) {
        const absolute = isAbsolute(changed) ? changed : resolve(appDir, changed);
        if (!absolute.endsWith(".wrn")) continue;
        try {
          const source = readFileSync(absolute, "utf8");
          const declaration = /\b(global|page)\s+store\s+([A-Za-z_$][\w$]*)\s*\{/.exec(source);
          if (!declaration) continue;
          storeUpdates.push({
            name: declaration[2]!,
            kind: declaration[1]!,
            url: await wrnBrowserArtifactUrlAsync(absolute),
          });
        } catch (error) {
          console.warn(`[wrnexus] failed to prepare store HMR for ${absolute}`, error);
        }
      }
      if (storeUpdates.length) {
        hub.broadcastJson({ type: "store-update", version: Date.now(), stores: storeUpdates });
      }
      const onlyStores =
        storeUpdates.length > 0 &&
        files.every((changed) => {
          const absolute = isAbsolute(changed) ? changed : resolve(appDir, changed);
          if (!absolute.endsWith(".wrn")) return false;
          try {
            return /\b(?:global|page)\s+store\s+[A-Za-z_$][\w$]*\s*\{/.test(
              readFileSync(absolute, "utf8"),
            );
          } catch {
            return false;
          }
        });
      if (!onlyStores) hub.reload();
      hub.broadcastJson({
        channel: "toolbar",
        type: "toolbar:scan",
        reason: "source-change",
        files,
      });
    };
    const packageWatchDirs = [
      dirname(uiStylesPath),
      ...componentDirs,
      ...pluginContributions.clientRuntimes.flatMap((runtime) =>
        runtime.entry ? [dirname(runtime.entry)] : [],
      ),
      ...pluginContributions.assets.flatMap((asset) => (asset.entry ? [dirname(asset.entry)] : [])),
      ...pluginContributions.styles.flatMap((style) =>
        [style.source, style.entry ? dirname(style.entry) : undefined].filter(
          (value): value is string => !!value,
        ),
      ),
      ...pluginContributions.routes.map((route) => dirname(route.entry)),
      ...pluginContributions.middleware.map((file) => dirname(file)),
    ];
    watcher = startWatcher({
      appDir,
      extraDirs: packageWatchDirs,
      hub,
      assets,
      devToolbarCollector,
      onHotChange: hotUpdate,
    });
  }

  const boundPort = server.port ?? port;
  return {
    port: boundPort,
    hostname,
    url: `http://${displayHost}:${boundPort}`,
    router,
    stop: () => {
      watcher?.close();
      unsubscribeDevToolbar?.();
      server.stop();
      void pluginRunner.hook("shutdown");
      rmSync(cacheDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
    },
  };
}

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";
// Internal: called only by the generated `.authz-setup.ts` module (see
// packages/cli/src/build.ts) to populate the authorization catalog before any
// other static import — including app middleware — evaluates. Not meant for
// direct use by application code.
export { applyAuthzManifestEarly } from "./prod.ts";
export type { AuthzManifestEntry } from "./prod.ts";
export { toRequest, writeResponse, nodeListener, serveNode } from "./adapters/node.ts";
export type { FetchHandler } from "./adapters/node.ts";
