import { validateUrl } from "@wrnexus/security";

export type ImageFormat = "avif" | "webp" | "jpeg" | "png" | "original";

export interface ImageLoaderInput {
  src: string;
  width: number;
  quality?: number;
  format?: ImageFormat;
}

export type ImageLoader = (input: ImageLoaderInput) => string;

function safeSvgColor(value: string, name: string): string {
  const color = value.trim();
  if (color.length > 128) throw new TypeError(`Unsafe ${name} image placeholder color.`);
  if (
    /^#[0-9a-f]{3,8}$/i.test(color) ||
    /^(?:rgb|hsl)a?\([0-9.,%\s/+-]+\)$/i.test(color) ||
    /^var\(--[A-Za-z0-9_-]+\)$/.test(color) ||
    /^[A-Za-z]+$/.test(color)
  ) {
    return color;
  }
  throw new TypeError(`Unsafe ${name} image placeholder color.`);
}

function escapeHtmlAttribute(value: string): string {
  return value
    .replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/"/g, "&quot;")
    .replace(/'/g, "&#39;");
}

export interface ImagePolicy {
  remoteHosts?: string[];
  allowedProtocols?: string[];
  maxWidth?: number;
  maxQuality?: number;
}

export interface ResponsiveImageOptions extends ImagePolicy {
  src: string;
  alt: string;
  width: number;
  height: number;
  widths?: number[];
  sizes?: string;
  quality?: number;
  format?: ImageFormat;
  loading?: "eager" | "lazy";
  fetchPriority?: "high" | "low" | "auto";
  decoding?: "async" | "sync" | "auto";
  loader?: ImageLoader;
  class?: string;
}

export interface ResponsiveImageAttributes {
  src: string;
  srcset?: string;
  sizes?: string;
  alt: string;
  width: string;
  height: string;
  loading: "eager" | "lazy";
  decoding: "async" | "sync" | "auto";
  fetchpriority?: "high" | "low" | "auto";
  class?: string;
}

export interface ImageAuditInput {
  src: string;
  width?: number;
  height?: number;
  renderedWidth?: number;
  bytes?: number;
  loading?: string;
  fetchPriority?: string;
  isLcp?: boolean;
}

export interface ImageAuditIssue {
  code: string;
  severity: "error" | "warning" | "info";
  message: string;
}

export const defaultImageLoader: ImageLoader = ({ src, width, quality, format }) => {
  const hashIndex = src.indexOf("#");
  const source = hashIndex >= 0 ? src.slice(0, hashIndex) : src;
  const hash = hashIndex >= 0 ? src.slice(hashIndex) : "";
  const separator = source.includes("?") ? "&" : "?";
  const params = new URLSearchParams({ w: String(width) });
  if (quality !== undefined) params.set("q", String(quality));
  if (format && format !== "original") params.set("format", format);
  return `${source}${separator}${params.toString()}${hash}`;
};

function validateSource(src: string, policy: ImagePolicy): void {
  if (/^(?:https?:)?\/\//i.test(src)) {
    validateUrl(src.startsWith("//") ? `https:${src}` : src, {
      allowRelative: false,
      allowedProtocols: policy.allowedProtocols ?? ["https:"],
      allowedHosts: policy.remoteHosts,
      allowCredentials: false,
    });
  } else {
    validateUrl(src, {
      base: "https://wrnexus.invalid",
      allowRelative: true,
      allowedProtocols: ["https:"],
      allowCredentials: false,
    });
  }
}

export function createResponsiveImage(options: ResponsiveImageOptions): ResponsiveImageAttributes {
  validateSource(options.src, options);
  if (!Number.isFinite(options.width) || options.width <= 0) {
    throw new RangeError("Image width must be a positive number.");
  }
  if (!Number.isFinite(options.height) || options.height <= 0) {
    throw new RangeError("Image height must be a positive number.");
  }
  const maxWidth = options.maxWidth ?? 8_192;
  const maxQuality = options.maxQuality ?? 100;
  if (!Number.isFinite(maxWidth) || maxWidth <= 0)
    throw new RangeError("Image maxWidth must be positive.");
  if (!Number.isFinite(maxQuality) || maxQuality <= 0)
    throw new RangeError("Image maxQuality must be positive.");
  const quality = Math.min(maxQuality, Math.max(1, Math.round(options.quality ?? 80)));
  const widths = [...new Set(options.widths ?? [options.width])]
    .map((width) => Math.round(width))
    .filter((width) => width > 0 && width <= maxWidth)
    .sort((a, b) => a - b);
  if (!widths.includes(Math.round(options.width)) && options.width <= maxWidth) {
    widths.push(Math.round(options.width));
    widths.sort((a, b) => a - b);
  }
  const loader = options.loader ?? defaultImageLoader;
  const src = loader({
    src: options.src,
    width: Math.min(Math.round(options.width), maxWidth),
    quality,
    format: options.format,
  });
  const srcset =
    widths.length > 1
      ? widths
          .map(
            (width) =>
              `${loader({ src: options.src, width, quality, format: options.format })} ${width}w`,
          )
          .join(", ")
      : undefined;

  return {
    src,
    ...(srcset ? { srcset } : {}),
    ...(options.sizes ? { sizes: options.sizes } : {}),
    alt: options.alt,
    width: String(Math.round(options.width)),
    height: String(Math.round(options.height)),
    loading: options.loading ?? (options.fetchPriority === "high" ? "eager" : "lazy"),
    decoding: options.decoding ?? "async",
    ...(options.fetchPriority ? { fetchpriority: options.fetchPriority } : {}),
    ...(options.class ? { class: options.class } : {}),
  };
}

export function auditImage(input: ImageAuditInput): ImageAuditIssue[] {
  const issues: ImageAuditIssue[] = [];
  if (!input.width || !input.height) {
    issues.push({
      code: "WRN-IMAGE-DIMENSIONS",
      severity: "error",
      message: "Images must declare width and height to prevent layout shifts.",
    });
  }
  if (input.isLcp && input.loading === "lazy") {
    issues.push({
      code: "WRN-IMAGE-LCP-LAZY",
      severity: "error",
      message: "The LCP image must not be lazy-loaded.",
    });
  }
  if (input.isLcp && input.fetchPriority !== "high") {
    issues.push({
      code: "WRN-IMAGE-LCP-PRIORITY",
      severity: "warning",
      message: "Consider fetchpriority=high for the LCP image.",
    });
  }
  if (input.width && input.renderedWidth && input.width > input.renderedWidth * 2.5) {
    issues.push({
      code: "WRN-IMAGE-OVERSIZED",
      severity: "warning",
      message: "The source image is substantially wider than its rendered size.",
    });
  }
  if ((input.bytes ?? 0) > 1_000_000) {
    issues.push({
      code: "WRN-IMAGE-BYTES",
      severity: (input.bytes ?? 0) > 3_000_000 ? "error" : "warning",
      message: `Image transfer size is ${Math.round((input.bytes ?? 0) / 1024)} KiB.`,
    });
  }
  if (/\.(?:png|jpe?g)(?:\?|$)/i.test(input.src)) {
    issues.push({
      code: "WRN-IMAGE-MODERN-FORMAT",
      severity: "info",
      message: "Consider serving AVIF or WebP with a compatible fallback.",
    });
  }
  return issues;
}

export interface PictureSource {
  type: string;
  srcset: string;
  sizes?: string;
}

export interface PicturePlan {
  image: ResponsiveImageAttributes;
  sources: PictureSource[];
}

export function normalizeImageWidths(
  widths: readonly number[],
  options: { min?: number; max?: number } = {},
): number[] {
  const requestedMin = options.min ?? 16;
  const requestedMax = options.max ?? 8_192;
  if (!Number.isFinite(requestedMin) || !Number.isFinite(requestedMax)) {
    throw new RangeError("Image width bounds must be finite.");
  }
  const min = Math.max(1, Math.round(requestedMin));
  const max = Math.max(min, Math.round(requestedMax));
  return [...new Set(widths.map((width) => Math.round(width)))]
    .filter((width) => Number.isFinite(width) && width >= min && width <= max)
    .sort((left, right) => left - right);
}

export function createCdnImageLoader(
  baseUrl: string,
  options: {
    sourceParam?: string;
    widthParam?: string;
    qualityParam?: string;
    formatParam?: string;
  } = {},
): ImageLoader {
  const base = new URL(baseUrl);
  validateUrl(base, {
    allowRelative: false,
    allowedProtocols: ["https:"],
    allowCredentials: false,
  });
  return ({ src, width, quality, format }) => {
    const url = new URL(base);
    url.searchParams.set(options.sourceParam ?? "src", src);
    url.searchParams.set(options.widthParam ?? "w", String(width));
    if (quality !== undefined) url.searchParams.set(options.qualityParam ?? "q", String(quality));
    if (format && format !== "original")
      url.searchParams.set(options.formatParam ?? "format", format);
    return url.toString();
  };
}

export function createPathImageLoader(prefix = "/__wrnexus/image"): ImageLoader {
  validateUrl(prefix, {
    base: "https://wrnexus.invalid",
    allowRelative: true,
    allowedProtocols: ["https:"],
    allowCredentials: false,
  });
  return ({ src, width, quality, format }) => {
    const path = `${prefix.replace(/\/$/, "")}/${encodeURIComponent(src)}`;
    const query = new URLSearchParams({ w: String(width) });
    if (quality !== undefined) query.set("q", String(quality));
    if (format && format !== "original") query.set("format", format);
    return `${path}?${query}`;
  };
}

export function createPicture(
  options: ResponsiveImageOptions & { formats?: ImageFormat[] },
): PicturePlan {
  validateSource(options.src, options);
  const formats: ImageFormat[] = [...new Set<ImageFormat>(options.formats ?? ["avif", "webp"])];
  const widths = normalizeImageWidths(options.widths ?? [options.width], { max: options.maxWidth });
  const loader = options.loader ?? defaultImageLoader;
  const quality = Math.min(
    options.maxQuality ?? 100,
    Math.max(1, Math.round(options.quality ?? 80)),
  );
  const sources = formats
    .filter((format) => format !== "original")
    .map((format) => ({
      type: `image/${format === "jpeg" ? "jpeg" : format}`,
      srcset: widths
        .map((width) => `${loader({ src: options.src, width, quality, format })} ${width}w`)
        .join(", "),
      ...(options.sizes ? { sizes: options.sizes } : {}),
    }));
  return {
    image: createResponsiveImage({ ...options, widths, format: options.format ?? "original" }),
    sources,
  };
}

export function createBlurPlaceholder(
  options: { width?: number; height?: number; color?: string; accent?: string } = {},
): string {
  const requestedWidth = options.width ?? 16;
  const requestedHeight = options.height ?? 9;
  if (!Number.isFinite(requestedWidth) || !Number.isFinite(requestedHeight)) {
    throw new RangeError("Image placeholder dimensions must be finite.");
  }
  const width = Math.max(1, Math.min(512, Math.round(requestedWidth)));
  const height = Math.max(1, Math.min(512, Math.round(requestedHeight)));
  const color = safeSvgColor(options.color ?? "#e2e8f0", "primary");
  const accent = safeSvgColor(options.accent ?? "#cbd5e1", "accent");
  const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}"><defs><linearGradient id="g"><stop stop-color="${color}"/><stop offset="1" stop-color="${accent}"/></linearGradient></defs><rect width="100%" height="100%" fill="url(#g)"/></svg>`;
  return `data:image/svg+xml,${encodeURIComponent(svg)}`;
}

export function imagePreload(
  image: ResponsiveImageAttributes,
  options: { as?: string; type?: string; crossOrigin?: "anonymous" | "use-credentials" } = {},
): string {
  const attrs = [
    'rel="preload"',
    `as="${options.as ?? "image"}"`,
    `href="${escapeHtmlAttribute(image.src)}"`,
  ];
  if (image.srcset) attrs.push(`imagesrcset="${escapeHtmlAttribute(image.srcset)}"`);
  if (image.sizes) attrs.push(`imagesizes="${escapeHtmlAttribute(image.sizes)}"`);
  if (options.type) attrs.push(`type="${escapeHtmlAttribute(options.type)}"`);
  if (options.crossOrigin) attrs.push(`crossorigin="${options.crossOrigin}"`);
  return `<link ${attrs.join(" ")} />`;
}

export function imageCacheKey(input: ImageLoaderInput): string {
  const source = `${input.src}|${Math.round(input.width)}|${input.quality ?? ""}|${input.format ?? "original"}`;
  let hash = 0x811c9dc5;
  for (let index = 0; index < source.length; index++) {
    hash ^= source.charCodeAt(index);
    hash = Math.imul(hash, 0x01000193);
  }
  return `img-${(hash >>> 0).toString(36)}`;
}

export { imagePlugin, imageComponentsDir } from "./plugin.ts";
export type { ImagePluginOptions } from "./plugin.ts";
export { optimizeImage } from "./optimize.ts";
export type {
  ImageProcessor,
  ImageProcessorResult,
  OptimizeImageOptions,
  OptimizedImageManifest,
  OptimizedImageVariant,
} from "./optimize.ts";
