import { extname } from "node:path";
import type {
  ClientRuntimeDefinition,
  PackageAssetDefinition,
  PackageStyleDefinition,
  WrnexusPackageManifest,
} from "./types.ts";

const ID_PATTERN = /^[a-z0-9@][a-z0-9@/._-]*$/i;

export function assertContributionId(kind: string, id: string): void {
  if (!id || !ID_PATTERN.test(id) || id.includes("..")) {
    throw new Error(`WRN-PLUGIN-${kind.toUpperCase()}-ID: invalid id '${id}'.`);
  }
}

export function defaultClientRuntimePath(id: string): string {
  assertContributionId("runtime", id);
  return `/__wrnexus/assets/${encodeURIComponent(id.replace(/^@/, "").replace(/[/.@]+/g, "-"))}.js`;
}

export function defaultPackageAssetPath(asset: PackageAssetDefinition): string {
  assertContributionId("asset", asset.id);
  const extension = asset.entry ? extname(asset.entry) : "";
  const suffix = extension || extensionForContentType(asset.contentType);
  const id = encodeURIComponent(asset.id.replace(/^@/, "").replace(/[/.@]+/g, "-"));
  return `/__wrnexus/assets/${id}${suffix}`;
}

function extensionForContentType(contentType?: string): string {
  if (!contentType) return "";
  if (contentType.includes("javascript")) return ".js";
  if (contentType.includes("css")) return ".css";
  if (contentType.includes("json")) return ".json";
  if (contentType.includes("svg")) return ".svg";
  if (contentType.includes("wav")) return ".wav";
  if (contentType.includes("png")) return ".png";
  return "";
}

export function normalizeClientRuntime(
  runtime: ClientRuntimeDefinition,
): ClientRuntimeDefinition & {
  publicPath: string;
  type: "module" | "script";
  load: "eager" | "defer" | "idle";
  singleton: boolean;
} {
  assertContributionId("runtime", runtime.id);
  if (!runtime.entry && runtime.source === undefined) {
    throw new Error(`WRN-PLUGIN-RUNTIME-SOURCE: runtime '${runtime.id}' needs entry or source.`);
  }
  const publicPath = runtime.publicPath ?? defaultClientRuntimePath(runtime.id);
  if (!publicPath.startsWith("/") || publicPath.includes("..")) {
    throw new Error(`WRN-PLUGIN-RUNTIME-PATH: runtime '${runtime.id}' has an unsafe publicPath.`);
  }
  return {
    ...runtime,
    publicPath,
    type: runtime.type ?? "module",
    load: runtime.load ?? "defer",
    inject: runtime.inject ?? "body-end",
    singleton: runtime.singleton ?? true,
  };
}

export function normalizePackageAsset(
  asset: PackageAssetDefinition,
): PackageAssetDefinition & { publicPath: string } {
  assertContributionId("asset", asset.id);
  if (!asset.entry && asset.source === undefined) {
    throw new Error(`WRN-PLUGIN-ASSET-SOURCE: asset '${asset.id}' needs entry or source.`);
  }
  const publicPath = asset.publicPath ?? defaultPackageAssetPath(asset);
  if (!publicPath.startsWith("/") || publicPath.includes("..")) {
    throw new Error(`WRN-PLUGIN-ASSET-PATH: asset '${asset.id}' has an unsafe publicPath.`);
  }
  return { ...asset, publicPath };
}

export function definePackageManifest<T extends WrnexusPackageManifest>(manifest: T): T {
  if (manifest.name) assertContributionId("package", manifest.name);
  for (const runtime of manifest.clientRuntimes ?? []) normalizeClientRuntime(runtime);
  for (const asset of manifest.assets ?? []) normalizePackageAsset(asset);
  validateStyleIds(manifest.styles ?? []);
  for (const route of manifest.routes ?? []) {
    if (!route.path.startsWith("/") || route.path.includes("..") || !route.entry) {
      throw new Error(`WRN-PLUGIN-ROUTE: invalid ${route.kind} route '${route.path}'.`);
    }
  }
  for (const migration of manifest.migrations ?? []) {
    assertContributionId("migration", migration.id);
    if (!migration.entry && migration.source === undefined) {
      throw new Error(
        `WRN-PLUGIN-MIGRATION-SOURCE: migration '${migration.id}' needs entry or source.`,
      );
    }
  }
  return manifest;
}

export function validateStyleIds(styles: readonly PackageStyleDefinition[]): void {
  const seen = new Set<string>();
  for (const style of styles) {
    assertContributionId("style", style.id);
    if (seen.has(style.id)) throw new Error(`WRN-PLUGIN-STYLE-DUPLICATE: ${style.id}`);
    seen.add(style.id);
    if (!style.entry && !style.source) {
      throw new Error(`WRN-PLUGIN-STYLE-SOURCE: style '${style.id}' needs entry or source.`);
    }
  }
}

export function contentTypeForPath(path: string): string {
  const extension = extname(path).toLowerCase();
  switch (extension) {
    case ".js":
    case ".mjs":
      return "text/javascript; charset=utf-8";
    case ".css":
      return "text/css; charset=utf-8";
    case ".json":
      return "application/json; charset=utf-8";
    case ".svg":
      return "image/svg+xml; charset=utf-8";
    case ".png":
      return "image/png";
    case ".jpg":
    case ".jpeg":
      return "image/jpeg";
    case ".webp":
      return "image/webp";
    case ".wav":
      return "audio/wav";
    case ".mp3":
      return "audio/mpeg";
    case ".woff":
      return "font/woff";
    case ".woff2":
      return "font/woff2";
    default:
      return "application/octet-stream";
  }
}
