import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import { gzipSync } from "node:zlib";
import { resolveOnlyOfficeInternalUrl } from "@/lib/onlyoffice/internal-url";

export const dynamic = "force-dynamic";
export const runtime = "nodejs";

// 说明：ONLYOFFICE 文档服务器在编辑过程中会从 `/cache/*` 拉取二进制缓存（例如 Editor.bin）。
// 当我们通过 `/onlyoffice-server/*` 反代文档服务器时，这些 `/cache/*` 请求会落到 Next 上，
// 若未额外反代，会导致 404，进而触发 ONLYOFFICE “下载失败（-4）/无法打开文档”。
// 因此这里把 `/cache/*` 同样反代到本机 ONLYOFFICE。
const stripHopByHopHeaders = (headers: Headers) => {
  // 说明：Hop-by-hop headers 不应被代理转发/透传
  const hopByHop = [
    "connection",
    "keep-alive",
    "proxy-authenticate",
    "proxy-authorization",
    "te",
    "trailer",
    "transfer-encoding",
    "upgrade",
  ];
  for (const key of hopByHop) {
    headers.delete(key);
  }
};

const pickCacheControl = (pathParts: string[], contentType: string, method: string) => {
  const m = String(method || "").toUpperCase();
  if (m !== "GET" && m !== "HEAD") return "no-store";
  if (contentType.includes("text/html")) return "no-store";

  const p = `/${(pathParts ?? []).join("/")}`.toLowerCase();
  // 说明：/cache 主要是 ONLYOFFICE 运行期二进制缓存，适合短缓存提升性能，但不宜过长。
  if (p.includes("editor.bin") || p.endsWith(".bin")) {
    return "public, max-age=604800, stale-while-revalidate=86400, immutable";
  }
  return "public, max-age=86400, stale-while-revalidate=3600";
};

const buildStableEtag = (pathParts: string[], search: string) => {
  const p = `/${(pathParts ?? []).join("/")}`;
  const s = String(search || "");
  return `W/"mnote-oo-cache:${p}${s}"`;
};

const isStrongCache = (cc: string) => {
  const v = String(cc || "");
  return v.includes("max-age=604800");
};

const shouldGzip = (request: NextRequest, contentType: string) => {
  const ae = String(request.headers.get("accept-encoding") || "").toLowerCase();
  if (!ae.includes("gzip")) return false;

  const ct = String(contentType || "").toLowerCase();
  const compressible =
    ct.startsWith("text/") ||
    ct.includes("javascript") ||
    ct.includes("json") ||
    ct.includes("xml") ||
    ct.includes("svg");

  return compressible;
};

const proxyCache = async (request: NextRequest, pathParts: string[]) => {
  const onlyofficeInternalUrl = await resolveOnlyOfficeInternalUrl();
  const incomingUrl = new URL(request.url);
  const target = new URL(
    `${onlyofficeInternalUrl}/cache/${(pathParts ?? []).map(encodeURIComponent).join("/")}`,
  );
  target.search = incomingUrl.search;

  const headers = new Headers(request.headers);
  headers.delete("host");
  // 说明：让上游返回明文（identity），我们在此处按需 gzip 压缩，保证 content-encoding 与 body 一致。
  headers.set("accept-encoding", "identity");
  stripHopByHopHeaders(headers);

  const method = request.method.toUpperCase();
  const hasBody = !["GET", "HEAD"].includes(method);

  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  const init: any = {
    method,
    headers,
    body: hasBody ? request.body : undefined,
    duplex: "half",
    redirect: "follow",
  };

  const upstream = await fetch(target, init);

  const upstreamContentLength = upstream.headers.get("content-length");
  const outHeaders = new Headers(upstream.headers);
  stripHopByHopHeaders(outHeaders);
  outHeaders.delete("content-encoding");
  outHeaders.delete("content-length");
  const contentType = upstream.headers.get("content-type") || "";
  const cacheControl = pickCacheControl(pathParts, contentType, request.method);
  outHeaders.set("cache-control", cacheControl);
  outHeaders.delete("pragma");
  outHeaders.delete("expires");
  outHeaders.delete("set-cookie");

  if (isStrongCache(cacheControl)) {
    const etag = buildStableEtag(pathParts, incomingUrl.search);
    outHeaders.set("etag", etag);
    const inm = String(request.headers.get("if-none-match") || "").trim();
    if (inm && (inm === etag || inm.split(",").map((s) => s.trim()).includes(etag))) {
      return new NextResponse(null, { status: 304, headers: outHeaders });
    }
  }

  // 说明：同 onlyoffice-server 路由，对强缓存二进制改为定长响应，提升浏览器落盘缓存命中率。
  if (isStrongCache(cacheControl)) {
    outHeaders.delete("vary");
    outHeaders.set("vary", "Accept-Encoding");
    if (method === "HEAD") {
      if (upstreamContentLength) outHeaders.set("content-length", upstreamContentLength);
      return new NextResponse(null, { status: upstream.status, headers: outHeaders });
    }
    const buf = await upstream.arrayBuffer();

    if (shouldGzip(request, contentType)) {
      try {
        const gz = gzipSync(Buffer.from(buf), { level: 6 });
        outHeaders.set("content-encoding", "gzip");
        outHeaders.set("content-length", String(gz.byteLength));
        return new NextResponse(gz, { status: upstream.status, headers: outHeaders });
      } catch {
        // ignore
      }
    }

    outHeaders.set("content-length", String(buf.byteLength));
    return new NextResponse(buf, { status: upstream.status, headers: outHeaders });
  }

  return new NextResponse(upstream.body, { status: upstream.status, headers: outHeaders });
};

type RouteCtx = { params: Promise<{ path: string[] }> };

export async function GET(request: NextRequest, ctx: RouteCtx) {
  const { path } = await ctx.params;
  return proxyCache(request, path ?? []);
}

export async function HEAD(request: NextRequest, ctx: RouteCtx) {
  const { path } = await ctx.params;
  return proxyCache(request, path ?? []);
}

export async function OPTIONS(request: NextRequest, ctx: RouteCtx) {
  const { path } = await ctx.params;
  return proxyCache(request, path ?? []);
}
