/**
 * 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,
  existsSync,
} from "node:fs";
import { dirname, join, basename, extname, relative, resolve } from "node:path";
import {
  compile,
  generate,
  generateTargets,
  resolveWrnImports,
  type PageAst,
} 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>();
const browserArtifactPaths = new Map<string, string>();

type ImportMode = "legacy" | "compatible" | "explicit";
interface CompileImportOptions {
  mode: ImportMode;
  aliases: Record<string, string>;
  autoImport: boolean;
}
const compileImportOptions = new Map<string, CompileImportOptions>();
const warnedImportDiagnostics = new Set<string>();

interface DevCompilerPipeline {
  transformAst(ast: PageAst, file: string): Promise<PageAst>;
  transformCode(code: string, file: string): Promise<string>;
  virtualModules: Map<string, string>;
}
let devCompilerPipeline: DevCompilerPipeline | null = null;

/** Install the configured plugin compiler pipeline for development compilation. */
export function setDevCompilerPipeline(pipeline: DevCompilerPipeline | null): void {
  devCompilerPipeline = pipeline;
}

export function setCompileImportOptions(
  appRoot: string,
  options: { mode?: ImportMode; aliases?: Record<string, string>; autoImport?: boolean } = {},
): void {
  compileImportOptions.set(resolve(appRoot), {
    mode: options.mode ?? "compatible",
    aliases: { "@": "./app", ...(options.aliases ?? {}) },
    autoImport: options.autoImport ?? true,
  });
}

const compileInProgress = new Map<string, WrnCompileArtifacts>();

function isApplicationImportSource(
  source: string,
  aliases: Record<string, string> | undefined,
): boolean {
  if (source.startsWith(".")) return true;
  return Object.keys(aliases ?? {}).some(
    (alias) => source === alias || source.startsWith(`${alias}/`),
  );
}

function projectRootForFile(file: string): string {
  let current = dirname(resolve(file));
  while (true) {
    if (existsSync(join(current, "app"))) return current;
    const parent = dirname(current);
    if (parent === current) return dirname(resolve(file));
    current = parent;
  }
}

function importOptionsHash(file: string): string {
  const root = resolve(projectRootForFile(file));
  const options = compileImportOptions.get(root) ?? {
    mode: "compatible" as const,
    aliases: { "@": "./app" },
    autoImport: true,
  };
  const aliases = Object.fromEntries(
    Object.entries(options.aliases).sort(([left], [right]) => left.localeCompare(right)),
  );
  return hashPath(JSON.stringify({ ...options, aliases }));
}

function rewriteArtifactImports(
  code: string,
  ast: PageAst,
  importer: string,
  target: "main" | "server" | "browser",
): string {
  if (!ast.structuredImports.length) return code;
  const root = projectRootForFile(importer);
  const importOptions = compileImportOptions.get(resolve(root)) ?? {
    mode: "compatible" as const,
    aliases: { "@": "./app" },
    autoImport: true,
  };
  const resolved = resolveWrnImports(ast.structuredImports, importer, {
    appRoot: root,
    mode: importOptions.mode,
    aliases: importOptions.aliases,
  });
  let output = code;
  for (const entry of resolved) {
    if (entry.diagnostic) {
      const key = `${importer}:${entry.diagnostic.code}:${entry.declaration.source}`;
      if (entry.diagnostic.severity === "error") {
        throw new Error(`${entry.diagnostic.code}: ${entry.diagnostic.message}`);
      }
      if (!warnedImportDiagnostics.has(key)) {
        warnedImportDiagnostics.add(key);
        console.warn(`[wrnexus] ${entry.diagnostic.code}: ${entry.diagnostic.message}`);
      }
    }
    if (!entry.resolved || !entry.declaration.source) continue;
    if (!isApplicationImportSource(entry.declaration.source, importOptions.aliases)) continue;
    let replacement = entry.resolved;
    if (entry.resolved.endsWith(".wrn")) {
      const dependencySource = readFileSync(entry.resolved, "utf8");
      const isStore = /\b(?:global|page)\s+store\s+[A-Za-z_$][\w$]*\s*\{/.test(dependencySource);
      const dependency = compileWireArtifacts(
        entry.resolved,
        moduleVersions.get(entry.resolved) ?? 0,
      );
      if (target === "browser") {
        if (!isStore) {
          // Components and layouts are compile-time dependencies in browser modules.
          output = output.replace(entry.declaration.raw, "");
          continue;
        }
        replacement = dependency.browser;
      } else {
        replacement = target === "server" ? dependency.server : dependency.main;
      }
    }
    const escaped = entry.declaration.source.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
    const specifier =
      target === "browser"
        ? (() => {
            const cacheDir = compileCacheDir ?? join(dirname(importer), ".wrnexus");
            const relativeTarget = relative(cacheDir, replacement).replace(/\\/g, "/");
            return relativeTarget.startsWith(".") ? relativeTarget : `./${relativeTarget}`;
          })()
        : replacement.startsWith("/")
          ? replacement
          : pathToFileURL(replacement).href;
    output = output.replace(new RegExp(`(["'])${escaped}\\1`, "g"), JSON.stringify(specifier));
  }
  return output;
}

async function rewriteArtifactImportsAsync(
  code: string,
  ast: PageAst,
  importer: string,
  target: "main" | "server" | "browser",
): Promise<string> {
  let output = code;
  for (const [id, replacement] of devCompilerPipeline?.virtualModules ?? []) {
    const escaped = id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
    const specifier =
      target === "browser"
        ? (() => {
            const cacheDir = compileCacheDir ?? join(dirname(importer), ".wrnexus");
            const relativeTarget = relative(cacheDir, replacement).replace(/\\/g, "/");
            return relativeTarget.startsWith(".") ? relativeTarget : `./${relativeTarget}`;
          })()
        : pathToFileURL(replacement).href;
    output = output.replace(new RegExp(`(["'])${escaped}\\1`, "g"), JSON.stringify(specifier));
  }
  if (!ast.structuredImports.length) return output;
  const root = projectRootForFile(importer);
  const importOptions = compileImportOptions.get(resolve(root)) ?? {
    mode: "compatible" as const,
    aliases: { "@": "./app" },
    autoImport: true,
  };
  const resolved = resolveWrnImports(ast.structuredImports, importer, {
    appRoot: root,
    mode: importOptions.mode,
    aliases: importOptions.aliases,
  });
  for (const entry of resolved) {
    if (entry.diagnostic?.severity === "error") throw new Error(entry.diagnostic.message);
    if (!entry.resolved || !entry.declaration.source) continue;
    if (!isApplicationImportSource(entry.declaration.source, importOptions.aliases)) continue;
    let replacement = entry.resolved;
    if (replacement.endsWith(".wrn")) {
      const dependencySource = readFileSync(replacement, "utf8");
      const isStore = /\b(?:global|page)\s+store\s+[A-Za-z_$][\w$]*\s*\{/.test(dependencySource);
      const dependency = await compileWireArtifactsAsync(
        replacement,
        moduleVersions.get(replacement) ?? 0,
      );
      if (target === "browser") {
        if (!isStore) {
          output = output.replace(entry.declaration.raw, "");
          continue;
        }
        replacement = dependency.browser;
      } else replacement = target === "server" ? dependency.server : dependency.main;
    }
    const escaped = entry.declaration.source.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
    const specifier =
      target === "browser"
        ? (() => {
            const cacheDir = compileCacheDir ?? join(dirname(importer), ".wrnexus");
            const relativeTarget = relative(cacheDir, replacement).replace(/\\/g, "/");
            return relativeTarget.startsWith(".") ? relativeTarget : `./${relativeTarget}`;
          })()
        : replacement.startsWith("/")
          ? replacement
          : pathToFileURL(replacement).href;
    output = output.replace(new RegExp(`(["'])${escaped}\\1`, "g"), JSON.stringify(specifier));
  }
  return output;
}

export async function loadModule(file: string): Promise<Record<string, unknown>> {
  file = resolve(file);
  let mod = moduleCache.get(file);
  if (!mod) {
    mod = (async () => {
      const version = moduleVersions.get(file) ?? 0;
      // `.wrn` files are compiled to TypeScript first, then imported.
      let target = file.endsWith(".wrn")
        ? (await compileWireArtifactsAsync(file, version)).main
        : 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.
      let imported = import(pathToFileURL(target).href) as Promise<Record<string, unknown>>;
      if (temporary) {
        imported = imported.finally(() => {
          try {
            unlinkSync(target);
          } catch {
            /* best-effort cleanup after Bun has loaded the module */
          }
        });
      }
      return imported;
    })();
    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;
const WRN_COMPILE_CACHE_VERSION = "v3";

/**
 * 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 | null): 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 importedValueBindings(ast: PageAst): Set<string> {
  const names = new Set<string>();
  for (const entry of ast.structuredImports) {
    if (entry.typeOnly) continue;
    if (entry.defaultImport) names.add(entry.defaultImport);
    if (entry.namespaceImport) names.add(entry.namespaceImport);
    for (const item of entry.namedImports) if (!item.typeOnly) names.add(item.local);
  }
  return names;
}

function validateConfiguredImports(source: string, ast: PageAst, file: string): void {
  const root = projectRootForFile(file);
  const options = compileImportOptions.get(resolve(root));
  if (!options || options.mode === "legacy") return;
  const imported = importedValueBindings(ast);
  const usedComponents = new Set(
    Array.from(source.matchAll(/<([A-Z][A-Za-z0-9_$]*)\b/g), (match) => match[1]!),
  );
  const compilerBuiltins = new Set([
    "Async",
    "Component",
    "Error",
    "Loading",
    "Portal",
    "Success",
    "Transition",
  ]);
  const missing = [...usedComponents].filter(
    (name) => !compilerBuiltins.has(name) && !imported.has(name),
  );
  if (ast.layoutIsSymbol && ast.layout && !imported.has(ast.layout)) missing.push(ast.layout);
  if (!missing.length) return;
  const unique = [...new Set(missing)];
  const message = `WRN-IMPORT-IMPLICIT: ${file} uses ${unique.join(", ")} without explicit imports.`;
  if (options.mode === "explicit") throw new Error(message);
  const key = `${file}:WRN-IMPORT-IMPLICIT:${unique.join(",")}`;
  if (!warnedImportDiagnostics.has(key)) {
    warnedImportDiagnostics.add(key);
    console.warn(`[wrnexus] ${message}`);
  }
}

export interface WrnCompileArtifacts {
  main: string;
  browser: string;
  server: string;
  declarations: string;
  contract: string;
  rpc: string;
}

export interface WrnCompileMetrics {
  hits: number;
  misses: number;
  compilations: number;
  errors: number;
  totalDurationMs: number;
  lastDurationMs: number;
}

const compileMetrics: WrnCompileMetrics = {
  hits: 0,
  misses: 0,
  compilations: 0,
  errors: 0,
  totalDurationMs: 0,
  lastDurationMs: 0,
};
const asyncCompileInProgress = new Map<string, Promise<WrnCompileArtifacts>>();

async function bundleBrowserArtifact(
  code: string,
  file: string,
  cacheDir: string,
  stem: string,
): Promise<string> {
  const hasModuleImport = /(?:^|\n)\s*import(?:\s|["'])|\bimport\s*\(/m.test(code);
  if (!hasModuleImport || code.includes("wrnexus-client-bundled")) return code;
  const bun = (globalThis as any).Bun;
  if (!bun?.build) {
    throw new Error(
      `WRN-CLIENT-BUNDLE: ${file} has browser imports, but the Bun bundler is unavailable.`,
    );
  }
  const entry = join(cacheDir, `${stem}.browser-entry.mjs`);
  writeFileSync(entry, code, "utf8");
  try {
    const result = await bun.build({
      entrypoints: [entry],
      target: "browser",
      format: "esm",
      splitting: false,
      minify: false,
      sourcemap: "inline",
    });
    if (!result.success || !result.outputs?.length) {
      const detail = (result.logs ?? []).map(String).join("\n");
      throw new Error(`WRN-CLIENT-BUNDLE: failed to bundle ${file}${detail ? `\n${detail}` : ""}`);
    }
    return `// wrnexus-client-bundled\n${await result.outputs[0].text()}`;
  } finally {
    try {
      unlinkSync(entry);
    } catch {
      // Best-effort cleanup; cache pruning removes stale temporary entries.
    }
  }
}

export function compileWireArtifactsAsync(file: string, version = 0): Promise<WrnCompileArtifacts> {
  const key = `${file}:${version}`;
  const active = asyncCompileInProgress.get(key);
  if (active) return active;
  const task = (async () => {
    if (!devCompilerPipeline) {
      const artifacts = compileWireArtifacts(file, version);
      const code = readFileSync(artifacts.browser, "utf8");
      const bundled = await bundleBrowserArtifact(
        code,
        file,
        dirname(artifacts.browser),
        basename(artifacts.browser, ".client.mjs"),
      );
      if (bundled !== code) writeFileSync(artifacts.browser, bundled, "utf8");
      return artifacts;
    }
    const cacheDir = compileCacheDir ?? join(dirname(file), ".wrnexus");
    const name = basename(file).replace(/\.wrn$/, "");
    const suffix = version ? `-hmr-${version}` : "";
    const source = readFileSync(file, "utf8");
    // Plugin output affects the artifact, so use a separate cache generation.
    const stem = `${name}-${WRN_COMPILE_CACHE_VERSION}-plugin-${hashPath(file)}-${hashPath(source)}-${importOptionsHash(file)}${suffix}`;
    const artifacts: WrnCompileArtifacts = {
      main: join(cacheDir, `${stem}.wrn.ts`),
      browser: join(cacheDir, `${stem}.client.mjs`),
      server: join(cacheDir, `${stem}.server.ts`),
      declarations: join(cacheDir, `${stem}.d.ts`),
      contract: join(cacheDir, `${stem}.contract.json`),
      rpc: join(cacheDir, `${stem}.rpc.json`),
    };
    const result = compile(source, file);
    validateConfiguredImports(source, result.ast, file);
    const ast = await devCompilerPipeline!.transformAst(result.ast, file);
    const targets = generateTargets(ast);
    mkdirSync(cacheDir, { recursive: true });
    const browserPath = `/__wrnexus/client/${stem}.mjs`;
    const outputs = {
      main: `// compiled from .wrn\n${generate(ast)}`.replaceAll(
        "__WRNEXUS_CLIENT_MODULE__",
        browserPath,
      ),
      browser: targets.browser,
      server: targets.server,
      declarations: targets.declarations,
    };
    for (const target of ["main", "browser", "server"] as const) {
      const rewritten = await rewriteArtifactImportsAsync(outputs[target], ast, file, target);
      let transformed = await devCompilerPipeline!.transformCode(rewritten, file);
      if (target === "browser") {
        transformed = await bundleBrowserArtifact(transformed, file, cacheDir, stem);
      }
      writeFileSync(artifacts[target], transformed, "utf8");
    }
    writeFileSync(
      artifacts.declarations,
      await devCompilerPipeline!.transformCode(outputs.declarations, file),
      "utf8",
    );
    writeFileSync(artifacts.contract, JSON.stringify(targets.contract, null, 2) + "\n", "utf8");
    writeFileSync(artifacts.rpc, JSON.stringify(targets.rpc, null, 2) + "\n", "utf8");
    browserArtifactPaths.set(browserPath, artifacts.browser);
    compileMetrics.compilations++;
    return artifacts;
  })().finally(() => asyncCompileInProgress.delete(key));
  asyncCompileInProgress.set(key, task);
  return task;
}

export function getWrnCompileMetrics(): Readonly<WrnCompileMetrics> {
  return { ...compileMetrics };
}

export function resetWrnCompileMetrics(): void {
  Object.assign(compileMetrics, {
    hits: 0,
    misses: 0,
    compilations: 0,
    errors: 0,
    totalDurationMs: 0,
    lastDurationMs: 0,
  });
}

export function compileWireArtifacts(file: string, version = 0): WrnCompileArtifacts {
  const active = compileInProgress.get(file);
  if (active) return active;
  const cacheDir = compileCacheDir ?? join(dirname(file), ".wrnexus");
  const name = basename(file).replace(/\.wrn$/, "");
  const suffix = version ? `-hmr-${version}` : "";
  const source = readFileSync(file, "utf8");
  const stem = `${name}-${WRN_COMPILE_CACHE_VERSION}-${hashPath(file)}-${hashPath(source)}-${importOptionsHash(file)}${suffix}`;
  const artifacts: WrnCompileArtifacts = {
    main: join(cacheDir, `${stem}.wrn.ts`),
    browser: join(cacheDir, `${stem}.client.mjs`),
    server: join(cacheDir, `${stem}.server.ts`),
    declarations: join(cacheDir, `${stem}.d.ts`),
    contract: join(cacheDir, `${stem}.contract.json`),
    rpc: join(cacheDir, `${stem}.rpc.json`),
  };
  compileInProgress.set(file, artifacts);
  try {
    try {
      if (Object.values(artifacts).every((path) => statSync(path).isFile())) {
        compileMetrics.hits++;
        browserArtifactPaths.set(`/__wrnexus/client/${stem}.mjs`, artifacts.browser);
        return artifacts;
      }
    } catch {
      // Compile missing artifact set below.
    }
    compileMetrics.misses++;
    const started = performance.now();
    try {
      const result = compile(source, file);
      validateConfiguredImports(source, result.ast, file);
      const targets = generateTargets(result.ast);
      mkdirSync(cacheDir, { recursive: true });
      const browserPath = `/__wrnexus/client/${stem}.mjs`;
      const mainCode = rewriteArtifactImports(
        result.code.replaceAll("__WRNEXUS_CLIENT_MODULE__", browserPath),
        result.ast,
        file,
        "main",
      );
      writeFileSync(artifacts.main, mainCode, "utf8");
      writeFileSync(
        artifacts.browser,
        rewriteArtifactImports(targets.browser, result.ast, file, "browser"),
        "utf8",
      );
      browserArtifactPaths.set(browserPath, artifacts.browser);
      writeFileSync(
        artifacts.server,
        rewriteArtifactImports(targets.server, result.ast, file, "server"),
        "utf8",
      );
      writeFileSync(artifacts.declarations, targets.declarations, "utf8");
      writeFileSync(artifacts.contract, JSON.stringify(targets.contract, null, 2) + "\n", "utf8");
      writeFileSync(artifacts.rpc, JSON.stringify(targets.rpc, null, 2) + "\n", "utf8");
      compileMetrics.compilations++;
      return artifacts;
    } catch (error) {
      compileMetrics.errors++;
      throw error;
    } finally {
      const duration = performance.now() - started;
      compileMetrics.lastDurationMs = duration;
      compileMetrics.totalDurationMs += duration;
    }
  } finally {
    compileInProgress.delete(file);
  }
}

export async function loadWrnServerModule(file: string): Promise<Record<string, unknown>> {
  const version = moduleVersions.get(file) ?? 0;
  const artifact = compileWireArtifacts(file, version).server;
  return import(pathToFileURL(artifact).href) as Promise<Record<string, unknown>>;
}

export function wrnBrowserArtifact(file: string): string {
  return compileWireArtifacts(file, moduleVersions.get(file) ?? 0).browser;
}

export function wrnBrowserArtifactUrl(file: string): string {
  const artifact = compileWireArtifacts(file, moduleVersions.get(file) ?? 0).browser;
  return `/__wrnexus/client/${basename(artifact).replace(/\.client\.mjs$/, ".mjs")}`;
}

/** Async browser artifact URL used by HMR so imported client modules are bundled before delivery. */
export async function wrnBrowserArtifactUrlAsync(file: string): Promise<string> {
  const artifact = (await compileWireArtifactsAsync(file, moduleVersions.get(file) ?? 0)).browser;
  return `/__wrnexus/client/${basename(artifact).replace(/\.client\.mjs$/, ".mjs")}`;
}

export function serveWrnBrowserArtifact(pathname: string): Response | null {
  const artifact = browserArtifactPaths.get(pathname);
  if (!artifact || !existsSync(artifact)) return null;
  return new Response(readFileSync(artifact, "utf8"), {
    headers: {
      "content-type": "text/javascript; charset=utf-8",
      "cache-control": "no-store, max-age=0",
      pragma: "no-cache",
      expires: "0",
    },
  });
}

/** Forget one module and force its next dynamic import to bypass Bun's import cache. */
export function invalidateModule(file: string): void {
  file = resolve(file);
  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();
}
