#!/usr/bin/env node
/**
 * 自定义 Next 生产 server：
 * - 解决 ONLYOFFICE 在 `/onlyoffice-server/*` 下的 WebSocket Upgrade 需求（socket.io / coauthoring）。
 * - 解决 Convex 在 HTTPS（frp/nginx）场景下浏览器不能连接 ws:// 的问题：通过同源 `/convex/*` 反代到本机 Convex。
 *
 * 用法：
 * - pnpm build
 * - pnpm start  （默认会执行本脚本）
 *
 * 依赖环境变量：
 * - ONLYOFFICE_INTERNAL_URL：可显式指定；未指定或失效时优先探测 8082，再回退 8081
 * - CONVEX_INTERNAL_URL：默认 http://127.0.0.1:3210
 */

const http = require("http");
const net = require("net");
const path = require("path");
const next = require("next");
const { parse: parseUrl } = require("url");

const ONLYOFFICE_PREFIX = "/onlyoffice-server";
const CONVEX_PREFIX = "/convex";
const DEFAULT_ONLYOFFICE_INTERNAL_URL = "http://127.0.0.1:8082";
const ONLYOFFICE_PROBE_PATH = "/web-apps/apps/api/documents/api.js";
const ONLYOFFICE_RESOLVE_CACHE_TTL_MS = 30_000;

let cachedOnlyOfficeInternalUrl = "";
let cachedOnlyOfficeInternalUrlAt = 0;
let pendingOnlyOfficeInternalUrl = null;

function readArgValue(flag) {
  const idx = process.argv.findIndex((x) => x === flag);
  if (idx === -1) return null;
  const v = process.argv[idx + 1];
  if (!v || v.startsWith("-")) return null;
  return v;
}

function resolvePort() {
  const fromArg = readArgValue("-p") || readArgValue("--port");
  const raw = fromArg || process.env.PORT || "3000";
  const n = Number(raw);
  return Number.isFinite(n) ? Math.max(1, Math.min(65535, Math.floor(n))) : 3000;
}

function resolveHostname() {
  return readArgValue("-H") || readArgValue("--hostname") || process.env.HOSTNAME || "0.0.0.0";
}

function isOnlyOfficePath(urlString) {
  try {
    const u = new URL(urlString, "http://localhost");
    return u.pathname === ONLYOFFICE_PREFIX || u.pathname.startsWith(`${ONLYOFFICE_PREFIX}/`);
  } catch {
    return false;
  }
}

function isConvexPath(urlString) {
  try {
    const u = new URL(urlString, "http://localhost");
    return u.pathname === CONVEX_PREFIX || u.pathname.startsWith(`${CONVEX_PREFIX}/`);
  } catch {
    return false;
  }
}

function normalizeOnlyOfficeInternalUrl(raw) {
  const value = String(raw || "").trim().replace(/\/+$/, "");
  if (!value) return "";
  try {
    const url = new URL(value);
    if (url.protocol !== "http:" && url.protocol !== "https:") return "";
    return url.toString().replace(/\/+$/, "");
  } catch {
    return "";
  }
}

function listOnlyOfficeInternalUrlCandidates() {
  const candidates = [];
  const push = (value) => {
    const normalized = normalizeOnlyOfficeInternalUrl(value);
    if (!normalized) return;
    if (!candidates.includes(normalized)) candidates.push(normalized);
  };

  push(process.env.ONLYOFFICE_INTERNAL_URL);

  for (const raw of String(process.env.ONLYOFFICE_INTERNAL_URL_CANDIDATES || "").split(",")) {
    push(raw);
  }

  push(DEFAULT_ONLYOFFICE_INTERNAL_URL);
  push("http://127.0.0.1:8081");
  push("http://localhost:8082");
  push("http://localhost:8081");

  return candidates.length > 0 ? candidates : [DEFAULT_ONLYOFFICE_INTERNAL_URL];
}

async function probeOnlyOfficeInternalUrl(candidate) {
  if (typeof fetch !== "function") return false;
  try {
    const probeUrl = new URL(ONLYOFFICE_PROBE_PATH, `${candidate}/`);
    const response = await fetch(probeUrl, {
      method: "HEAD",
      redirect: "follow",
      cache: "no-store",
      signal: AbortSignal.timeout(2500),
    });
    return response.ok;
  } catch {
    return false;
  }
}

async function resolveOnlyOfficeInternalUrl() {
  const now = Date.now();
  if (cachedOnlyOfficeInternalUrl && now - cachedOnlyOfficeInternalUrlAt < ONLYOFFICE_RESOLVE_CACHE_TTL_MS) {
    return new URL(`${cachedOnlyOfficeInternalUrl}/`);
  }

  if (!pendingOnlyOfficeInternalUrl) {
    pendingOnlyOfficeInternalUrl = (async () => {
      const candidates = listOnlyOfficeInternalUrlCandidates();
      for (const candidate of candidates) {
        if (await probeOnlyOfficeInternalUrl(candidate)) {
          return candidate;
        }
      }
      return candidates[0] || DEFAULT_ONLYOFFICE_INTERNAL_URL;
    })();
  }

  try {
    const resolved = await pendingOnlyOfficeInternalUrl;
    cachedOnlyOfficeInternalUrl = resolved;
    cachedOnlyOfficeInternalUrlAt = Date.now();
    return new URL(`${resolved}/`);
  } finally {
    pendingOnlyOfficeInternalUrl = null;
  }
}

function buildUpstreamRequestHead(req, targetUrl, prefix) {
  const incoming = new URL(req.url || "/", "http://localhost");
  const rawPath = incoming.pathname || "/";
  const stripped = rawPath === prefix ? "/" : rawPath.slice(prefix.length) || "/";

  const basePath = String(targetUrl.pathname || "/").replace(/\/+$/, "") || "";
  const upstreamPath = `${basePath}${stripped}`.replace(/\/{2,}/g, "/") + (incoming.search || "");

  const lines = [];
  lines.push(`${req.method || "GET"} ${upstreamPath} HTTP/1.1`);

  const headers = req.headers || {};
  const headerValue = (name) => {
    const v = headers[name];
    if (!v) return "";
    return Array.isArray(v) ? v[0] : String(v);
  };

  const originLike = headerValue("origin") || headerValue("referer") || "";
  const originUrl = (() => {
    try {
      if (!originLike) return null;
      return new URL(originLike);
    } catch {
      return null;
    }
  })();

  const forwardedHostRaw =
    headerValue("x-forwarded-host") || (originUrl ? originUrl.host : "") || headerValue("host") || "";
  const forwardedProto =
    (headerValue("x-forwarded-proto") || "").split(",")[0].trim() ||
    (originUrl ? originUrl.protocol.replace(":", "") : "") ||
    "http";
  const forwardedPort = (() => {
    const fromHeader = (headerValue("x-forwarded-port") || "").split(",")[0].trim();
    if (fromHeader) return fromHeader;
    const hostHasPort = forwardedHostRaw.includes(":") ? forwardedHostRaw.split(":").pop() : "";
    if (hostHasPort && /^\d+$/.test(hostHasPort)) return hostHasPort;
    return forwardedProto === "https" ? "443" : "80";
  })();
  const forwardedHost = forwardedHostRaw.split(",")[0].trim() || "localhost";

  for (const [k, v] of Object.entries(headers)) {
    if (!v) continue;
    const key = String(k);
    if (key.toLowerCase() === "host") continue;
    if (Array.isArray(v)) {
      lines.push(`${key}: ${v.join(", ")}`);
    } else {
      lines.push(`${key}: ${String(v)}`);
    }
  }

  lines.push(`x-forwarded-host: ${forwardedHost}`);
  lines.push(`x-forwarded-proto: ${forwardedProto}`);
  lines.push(`x-forwarded-port: ${forwardedPort}`);
  lines.push(`x-forwarded-prefix: ${prefix}`);
  lines.push(`Host: ${targetUrl.host}`);
  lines.push("");
  lines.push("");
  return lines.join("\r\n");
}

async function proxyOnlyOfficeUpgrade(req, socket, head) {
  let target;
  try {
    target = await resolveOnlyOfficeInternalUrl();
  } catch (err) {
    try {
      const msg = err && err.message ? String(err.message) : String(err || "");
      console.log("[prod-server][onlyoffice-ws] resolve error", msg);
    } catch {}
    try {
      socket.destroy();
    } catch {}
    return;
  }
  const port = Number(target.port) || (target.protocol === "https:" ? 443 : 80);

  const upstream = net.connect({ host: target.hostname, port }, () => {
    try {
      const reqHead = buildUpstreamRequestHead(req, target, ONLYOFFICE_PREFIX);
      upstream.write(reqHead);
      if (head && head.length > 0) upstream.write(head);
      socket.pipe(upstream);
      upstream.pipe(socket);
    } catch {
      try {
        socket.destroy();
      } catch {}
      try {
        upstream.destroy();
      } catch {}
    }
  });

  const onError = (err) => {
    try {
      const msg = err && err.message ? String(err.message) : String(err || "");
      console.log("[prod-server][onlyoffice-ws] proxy error", msg);
    } catch {}
    try {
      socket.destroy();
    } catch {}
    try {
      upstream.destroy();
    } catch {}
  };

  upstream.on("error", onError);
  socket.on("error", onError);
}

function resolveConvexInternalUrl() {
  const raw = (process.env.CONVEX_INTERNAL_URL || "http://127.0.0.1:3210").trim();
  try {
    return new URL(raw.replace(/\/+$/, "") + "/");
  } catch {
    return new URL("http://127.0.0.1:3210/");
  }
}

function proxyConvexUpgrade(req, socket, head) {
  const target = resolveConvexInternalUrl();
  const port = Number(target.port) || (target.protocol === "https:" ? 443 : 80);

  const upstream = net.connect({ host: target.hostname, port }, () => {
    try {
      const reqHead = buildUpstreamRequestHead(req, target, CONVEX_PREFIX);
      upstream.write(reqHead);
      if (head && head.length > 0) upstream.write(head);
      socket.pipe(upstream);
      upstream.pipe(socket);
    } catch {
      try {
        socket.destroy();
      } catch {}
      try {
        upstream.destroy();
      } catch {}
    }
  });

  const onError = (err) => {
    try {
      const msg = err && err.message ? String(err.message) : String(err || "");
      console.log("[prod-server][convex-ws] proxy error", msg);
    } catch {}
    try {
      socket.destroy();
    } catch {}
    try {
      upstream.destroy();
    } catch {}
  };

  upstream.on("error", onError);
  socket.on("error", onError);
}

function proxyConvexHttp(req, res) {
  const target = resolveConvexInternalUrl();
  const incoming = new URL(req.url || "/", "http://localhost");
  const rawPath = incoming.pathname || "/";
  const stripped = rawPath === CONVEX_PREFIX ? "/" : rawPath.slice(CONVEX_PREFIX.length) || "/";
  const upstreamPath = stripped + (incoming.search || "");

  const isHttps = target.protocol === "https:";
  const mod = isHttps ? require("https") : require("http");

  const headers = { ...(req.headers || {}) };
  headers.host = target.host;

  const forwardedHostRaw = String(headers["x-forwarded-host"] || req.headers.host || "");
  const forwardedHost = forwardedHostRaw.split(",")[0].trim() || "localhost";
  const forwardedProto =
    String(headers["x-forwarded-proto"] || "").split(",")[0].trim() ||
    (String(req.headers.origin || "").startsWith("https") ? "https" : "http");
  const forwardedPortRaw = String(headers["x-forwarded-port"] || "").split(",")[0].trim();
  const forwardedPort = (() => {
    if (forwardedPortRaw) return forwardedPortRaw;
    const hostHasPort = forwardedHost.includes(":") ? forwardedHost.split(":").pop() : "";
    if (hostHasPort && /^\d+$/.test(hostHasPort)) return hostHasPort;
    return forwardedProto === "https" ? "443" : "80";
  })();

  headers["x-forwarded-host"] = forwardedHost;
  headers["x-forwarded-proto"] = forwardedProto;
  headers["x-forwarded-port"] = forwardedPort;
  headers["x-forwarded-prefix"] = CONVEX_PREFIX;

  const upstreamReq = mod.request(
    {
      protocol: target.protocol,
      hostname: target.hostname,
      port: target.port || (isHttps ? 443 : 80),
      method: req.method,
      path: upstreamPath,
      headers,
    },
    (upstreamRes) => {
      res.writeHead(upstreamRes.statusCode || 502, upstreamRes.headers || {});
      upstreamRes.pipe(res);
    },
  );

  upstreamReq.on("error", (err) => {
    try {
      console.log("[prod-server][convex-http] proxy error", err && err.message ? err.message : String(err || ""));
    } catch {}
    try {
      res.statusCode = 502;
      res.end("Bad Gateway");
    } catch {}
  });

  req.pipe(upstreamReq);
}

async function main() {
  const port = resolvePort();
  const hostname = resolveHostname();
  const dev = false;

  const app = next({ dev, dir: path.join(__dirname, "..") });
  const handle = app.getRequestHandler();

  await app.prepare();
  const handleUpgrade = typeof app.getUpgradeHandler === "function" ? app.getUpgradeHandler() : null;

  const server = http.createServer((req, res) => {
    try {
      res.setHeader("x-mnote-prod-server", "1");
      res.setHeader("x-mnote-onlyoffice-ws-proxy", "1");
      res.setHeader("x-mnote-convex-ws-proxy", "1");
    } catch {
      // ignore
    }

    if (isConvexPath(req.url || "/")) {
      proxyConvexHttp(req, res);
      return;
    }

    const parsed = parseUrl(req.url || "/", true);
    handle(req, res, parsed);
  });

  server.on("upgrade", (req, socket, head) => {
    if (isConvexPath(req.url || "/")) {
      proxyConvexUpgrade(req, socket, head);
      return;
    }
    if (isOnlyOfficePath(req.url || "/")) {
      proxyOnlyOfficeUpgrade(req, socket, head);
      return;
    }
    if (handleUpgrade) {
      handleUpgrade(req, socket, head);
      return;
    }
    try {
      socket.destroy();
    } catch {
      // ignore
    }
  });

  server.listen(port, hostname, () => {
    resolveOnlyOfficeInternalUrl()
      .then((onlyofficeTarget) => {
        console.log(
          `[prod-server] ready http://${hostname}:${port} (ONLYOFFICE ws via ${ONLYOFFICE_PREFIX} -> ${onlyofficeTarget.toString().replace(/\/$/, "")}; Convex ws via ${CONVEX_PREFIX} -> ${process.env.CONVEX_INTERNAL_URL || "http://127.0.0.1:3210"})`,
        );
      })
      .catch(() => {
        console.log(
          `[prod-server] ready http://${hostname}:${port} (ONLYOFFICE ws via ${ONLYOFFICE_PREFIX} -> ${process.env.ONLYOFFICE_INTERNAL_URL || DEFAULT_ONLYOFFICE_INTERNAL_URL}; Convex ws via ${CONVEX_PREFIX} -> ${process.env.CONVEX_INTERNAL_URL || "http://127.0.0.1:3210"})`,
        );
      });
  });
}

main().catch((err) => {
  console.error(err instanceof Error ? err.stack : String(err));
  process.exit(1);
});

