/**
 * Request pipeline helpers: middleware execution and safe module loading.
 * These are deliberately runtime-agnostic (no Bun APIs) so they could run on
 * Node too.
 */

import { pathToFileURL } from "node:url";
import {
  copyFileSync,
  readFileSync,
  writeFileSync,
  mkdirSync,
  statSync,
  unlinkSync,
} from "node:fs";
import { dirname, join, basename, extname } from "node:path";
import { compileWireFile } from "@wrnexus/compiler";
import type { Context, Middleware } from "@wrnexus/core";

/**
 * Run an onion-style middleware chain, ending in `final` (the route handler).
 * Each middleware receives `next`; calling it advances the chain. A middleware
 * may short-circuit by returning a Response without calling `next`.
 */
export function runMiddleware(
  middlewares: Middleware[],
  ctx: Context,
  final: () => Promise<Response> | Response,
): Promise<Response> {
  let lastIndex = -1;

  const dispatch = (index: number): Promise<Response> => {
    if (index <= lastIndex) {
      return Promise.reject(new Error("next() called multiple times"));
    }
    lastIndex = index;
    const mw = middlewares[index];
    if (!mw) return Promise.resolve(final());
    return Promise.resolve(mw(ctx, () => dispatch(index + 1)));
  };

  return dispatch(0);
}

/**
 * Cache of imported route modules. Modules are only ever loaded from absolute
 * paths discovered during the startup scan — never from request input.
 */
const moduleCache = new Map<string, Promise<Record<string, unknown>>>();
const moduleVersions = new Map<string, number>();

export function loadModule(file: string): Promise<Record<string, unknown>> {
  let mod = moduleCache.get(file);
  if (!mod) {
    const version = moduleVersions.get(file) ?? 0;
    // `.wrn` files are compiled to TypeScript first, then imported.
    let target = file.endsWith(".wrn") ? compileWireToTs(file, version) : file;
    let temporary = false;
    // Bun intentionally caches local TS/JS modules by filesystem path and ignores
    // URL query strings. A short-lived versioned sibling keeps relative imports
    // correct while giving the changed module a genuinely new import identity.
    if (version && !file.endsWith(".wrn")) {
      const extension = extname(file);
      const stem = basename(file, extension);
      target = join(dirname(file), `${stem}.wrnexus-hmr-${version}${extension}`);
      copyFileSync(file, target);
      temporary = true;
    }
    // pathToFileURL handles Windows drive letters and spaces correctly.
    mod = import(pathToFileURL(target).href) as Promise<Record<string, unknown>>;
    if (temporary) {
      mod = mod.finally(() => {
        try {
          unlinkSync(target);
        } catch {
          /* best-effort cleanup after Bun has loaded the module */
        }
      });
    }
    moduleCache.set(file, mod);
  }
  return mod;
}

/**
 * A single cache dir for ALL `.wrn` compilation (set once at server start).
 * When unset, compilation falls back to a sibling `.wrnexus/` next to each file.
 */
let compileCacheDir: string | null = null;

/**
 * Point all `.wrn` compilation at ONE cache dir (typically `<appRoot>/.wrnexus`)
 * instead of scattering a `.wrnexus/` folder next to every `.wrn` source. Called
 * once by the dev server at startup.
 */
export function setCompileCacheDir(dir: string): void {
  compileCacheDir = dir;
}

/** FNV-1a hash of a string → short base36, to make unique flat cache filenames. */
function hashPath(s: string): string {
  let h = 0x811c9dc5;
  for (let i = 0; i < s.length; i++) {
    h ^= s.charCodeAt(i);
    h = Math.imul(h, 0x01000193);
  }
  return (h >>> 0).toString(36);
}

/**
 * Compile a `.wrn` file to a `.ts` file inside the shared `.wrnexus/` cache dir and
 * return the generated path. The cache dir is hidden, so the router never re-scans
 * it and the dev watcher ignores it. Output names are flat + hash-suffixed by the
 * absolute source path, so `.wrn` files from anywhere (the app AND node_modules UI
 * components) share one cache dir without colliding. Generated modules are
 * self-contained (no relative imports), so the cache location doesn't affect them.
 */
function compileWireToTs(file: string, version = 0): string {
  const cacheDir = compileCacheDir ?? join(dirname(file), ".wrnexus");
  const name = basename(file).replace(/\.wrn$/, "");
  const suffix = version ? `-hmr-${version}` : "";
  const out = join(cacheDir, `${name}-${hashPath(file)}${suffix}.wrn.ts`);

  // Skip recompiling when the on-disk cache is already newer than the source
  // (e.g. reused across dev restarts) — avoids a read + compile + write.
  try {
    if (statSync(out).mtimeMs >= statSync(file).mtimeMs) return out;
  } catch {
    /* cache missing → compile below */
  }

  const code = compileWireFile(readFileSync(file, "utf8"));
  mkdirSync(cacheDir, { recursive: true });
  writeFileSync(out, code, "utf8");
  return out;
}

/** Forget one module and force its next dynamic import to bypass Bun's import cache. */
export function invalidateModule(file: string): void {
  moduleCache.delete(file);
  moduleVersions.set(file, (moduleVersions.get(file) ?? 0) + 1);
}

/** Forget cached modules (used by build/dev tooling if needed). */
export function clearModuleCache(): void {
  for (const file of moduleCache.keys()) {
    moduleVersions.set(file, (moduleVersions.get(file) ?? 0) + 1);
  }
  moduleCache.clear();
}
