import type { Context, Middleware } from "@wrnexus/core";
import { etag, notModified } from "@wrnexus/core";
import { TagCache, type CacheSetOptions } from "./memory.ts";

export interface CachedResponse {
  status: number;
  statusText: string;
  headers: [string, string][];
  body: Uint8Array;
  etag: string;
}

export interface ResponseCacheOptions extends CacheSetOptions {
  cache?: TagCache<CachedResponse>;
  key?: (ctx: Context) => string;
  vary?: string[];
  shouldCache?: (ctx: Context, response: Response) => boolean;
  /**
   * Optional detached revalidator used for stale-while-revalidate. Middleware
   * `next()` is deliberately never called after a response has been returned,
   * because many middleware pipelines are single-use.
   */
  revalidate?: (ctx: Context) => Promise<Response>;
  onRevalidateError?: (error: unknown, ctx: Context) => void;
}

function defaultKey(ctx: Context, vary: string[]): string {
  const values = vary.map((name) => `${name.toLowerCase()}=${ctx.req.headers.get(name) ?? ""}`);
  return `${ctx.req.method}:${ctx.url.origin}${ctx.url.pathname}${ctx.url.search}|${values.join("|")}`;
}

function cacheable(ctx: Context, response: Response): boolean {
  if (ctx.req.method !== "GET" && ctx.req.method !== "HEAD") return false;
  if (response.status < 200 || response.status >= 400) return false;
  if (response.headers.has("set-cookie")) return false;
  const control = response.headers.get("cache-control") ?? "";
  return !/(?:^|,)\s*(?:no-store|private)(?:\s|,|$)/i.test(control);
}

function fromCached(ctx: Context, cached: CachedResponse, state: "fresh" | "stale"): Response {
  const headers = new Headers(cached.headers);
  headers.set("x-wrnexus-cache", state === "fresh" ? "HIT" : "STALE");
  headers.set("age", "0");
  if (notModified(ctx.req, cached.etag)) {
    return new Response(null, { status: 304, headers });
  }
  return new Response(ctx.req.method === "HEAD" ? null : cached.body.slice(), {
    status: cached.status,
    statusText: cached.statusText,
    headers,
  });
}

async function capture(response: Response): Promise<CachedResponse> {
  const body = new Uint8Array(await response.clone().arrayBuffer());
  const tag = response.headers.get("etag") ?? etag(body);
  const headers = new Headers(response.headers);
  headers.set("etag", tag);
  return {
    status: response.status,
    statusText: response.statusText,
    headers: [...headers.entries()],
    body,
    etag: tag,
  };
}

export function responseCache(options: ResponseCacheOptions = {}): Middleware {
  const cache = options.cache ?? new TagCache<CachedResponse>();
  const vary = options.vary ?? ["accept-encoding", "accept-language"];
  return async (ctx, next) => {
    if (ctx.req.method !== "GET" && ctx.req.method !== "HEAD") return next();
    const key = options.key?.(ctx) ?? defaultKey(ctx, vary);
    const hit = cache.lookup(key);
    if (hit.state === "fresh") return fromCached(ctx, hit.entry.value, "fresh");
    if (hit.state === "stale") {
      if (options.revalidate) {
        void options
          .revalidate(ctx)
          .then(async (response) => {
            if ((options.shouldCache ?? cacheable)(ctx, response)) {
              cache.set(key, await capture(response), options);
            }
          })
          .catch((error: unknown) => options.onRevalidateError?.(error, ctx));
      }
      return fromCached(ctx, hit.entry.value, "stale");
    }

    const response = await next();
    if (!(options.shouldCache ?? cacheable)(ctx, response)) {
      try {
        response.headers.set("x-wrnexus-cache", "BYPASS");
      } catch {
        // Immutable response.
      }
      return response;
    }
    const stored = await capture(response);
    cache.set(key, stored, options);
    const headers = new Headers(stored.headers);
    headers.set("x-wrnexus-cache", "MISS");
    return new Response(ctx.req.method === "HEAD" ? null : stored.body.slice(), {
      status: stored.status,
      statusText: stored.statusText,
      headers,
    });
  };
}
