import { readFile } from "node:fs/promises";
import type {
  ClientRuntimeDefinition,
  PackageAssetDefinition,
  PluginContributions,
} from "@wrnexus/plugin";
import { contentTypeForPath, normalizeClientRuntime, normalizePackageAsset } from "@wrnexus/plugin";
import type { ScriptAsset } from "@wrnexus/ssr";

export interface ServedPluginAsset {
  id: string;
  publicPath: string;
  entry?: string;
  source?: string | Uint8Array;
  contentType: string;
  immutable: boolean;
  runtime?: boolean;
  runtimeType?: "module" | "script";
  bundle?: boolean;
}

export interface ClientRuntimeAsset extends ClientRuntimeDefinition {
  publicPath: string;
}

function toBody(value: Uint8Array): ArrayBuffer {
  return Uint8Array.from(value).buffer;
}

export function pluginAssetsFromContributions(
  contributions: PluginContributions,
): ServedPluginAsset[] {
  const assets: ServedPluginAsset[] = [];
  for (const input of contributions.clientRuntimes) {
    const runtime = normalizeClientRuntime(input);
    assets.push({
      id: `runtime:${runtime.id}`,
      publicPath: runtime.publicPath!,
      entry: runtime.entry,
      source: runtime.source,
      contentType: "text/javascript; charset=utf-8",
      immutable: false,
      runtime: true,
      runtimeType: runtime.type,
      bundle: runtime.bundle,
    });
  }
  for (const input of contributions.assets) {
    const asset = normalizePackageAsset(input);
    assets.push({
      id: `asset:${asset.id}`,
      publicPath: asset.publicPath!,
      entry: asset.entry,
      source: asset.source,
      contentType: asset.contentType ?? contentTypeForPath(asset.entry ?? asset.publicPath!),
      immutable: asset.immutable ?? false,
    });
  }
  return assets;
}

export async function readPluginAsset(asset: ServedPluginAsset): Promise<string | ArrayBuffer> {
  if (typeof asset.source === "string") return asset.source;
  if (asset.source instanceof Uint8Array) return toBody(asset.source);
  if (!asset.entry) throw new Error(`Plugin asset '${asset.id}' has no source.`);

  const shouldBundle = asset.runtime && (asset.bundle ?? /\.[cm]?tsx?$/.test(asset.entry));
  if (!shouldBundle) return toBody(await readFile(asset.entry));

  const result = await Bun.build({
    entrypoints: [asset.entry],
    target: "browser",
    format: asset.runtimeType === "script" ? "iife" : "esm",
    minify: false,
    sourcemap: "inline",
  });
  if (!result.success || !result.outputs[0]) {
    throw new Error(
      `Plugin runtime '${asset.id}' failed to build:\n${result.logs.map(String).join("\n")}`,
    );
  }
  return toBody(new Uint8Array(await result.outputs[0].arrayBuffer()));
}

export async function servePluginAsset(
  assets: readonly ServedPluginAsset[],
  pathname: string,
  mode: "development" | "production",
): Promise<Response | null> {
  const asset = assets.find((entry) => entry.publicPath === pathname);
  if (!asset) return null;
  try {
    const body = await readPluginAsset(asset);
    return new Response(body, {
      headers: {
        "content-type": asset.contentType,
        "cache-control":
          mode === "production" && asset.immutable
            ? "public, max-age=31536000, immutable"
            : "no-cache",
        "x-content-type-options": "nosniff",
      },
    });
  } catch (error) {
    console.error(`[wrnexus] failed to serve plugin asset ${asset.publicPath}`, error);
    return new Response("Plugin asset unavailable", { status: 503 });
  }
}

export function runtimeScript(input: ClientRuntimeDefinition): ScriptAsset {
  const runtime = normalizeClientRuntime(input);
  return {
    src: runtime.publicPath!,
    type: runtime.type === "script" ? "classic" : "module",
    async: runtime.load === "eager" ? false : undefined,
    defer: runtime.load !== "eager",
    integrity: runtime.integrity,
    crossOrigin: runtime.crossOrigin,
    attributes: {
      "data-wrnexus-runtime-src": runtime.id,
      ...(runtime.attributes ?? {}),
    },
  };
}

export function runtimeIdsFromMarkup(body: string): Set<string> {
  const ids = new Set<string>();
  for (const match of body.matchAll(/\bdata-wrnexus-runtime\s*=\s*["']([^"']+)["']/gi)) {
    for (const id of match[1]!.split(/[\s,]+/)) {
      if (id) ids.add(id);
    }
  }
  return ids;
}

export function runtimeScriptsForMarkup(
  body: string,
  runtimes: readonly ClientRuntimeDefinition[] = [],
): ScriptAsset[] {
  const ids = runtimeIdsFromMarkup(body);
  return runtimes.filter((runtime) => ids.has(runtime.id)).map(runtimeScript);
}

export function mergePluginAssets(
  runtimes: readonly ClientRuntimeDefinition[],
  assets: readonly PackageAssetDefinition[],
): ServedPluginAsset[] {
  return pluginAssetsFromContributions({
    componentDirs: [],
    clientRuntimes: [...runtimes],
    assets: [...assets],
    styles: [],
    routes: [],
    middleware: [],
    migrations: [],
    directives: [],
    cliCommands: [],
    virtualModules: [],
    deploymentAdapters: [],
    configSchemas: [],
    documentation: [],
    typeDefinitions: [],
  });
}
