#!/usr/bin/env node
import http, { request as httpRequest } from "node:http";
import https, { request as httpsRequest } from "node:https";
import { randomBytes } from "node:crypto";

import {
  healthPayload,
  resolveHostPort,
} from "../../internal/dev-entrypoint/http.mjs";

const serviceId = "hwlab-edge-proxy";
const upstream = process.env.HWLAB_EDGE_UPSTREAM || "http://hwlab-cloud-api.hwlab-dev.svc.cluster.local:6667";
const proxyTimeoutMs = parseTimeout(process.env.HWLAB_EDGE_PROXY_TIMEOUT_MS, 1260000, {
  min: 1000,
  max: 2400000
});
const idleTimeoutSeconds = parseTimeout(process.env.HWLAB_EDGE_IDLE_TIMEOUT_SECONDS, 120, {
  min: 1,
  max: 255
});
const { host, port } = resolveHostPort({
  listenEnv: "HWLAB_EDGE_LISTEN",
  hostEnv: "HWLAB_EDGE_HOST",
  portEnv: "HWLAB_EDGE_PORT",
  fallbackPort: 6667
});

const server = http.createServer(handleHttpRequest);
server.on("upgrade", handleUpgrade);
server.keepAliveTimeout = idleTimeoutSeconds * 1000;
server.headersTimeout = Math.max(60000, server.keepAliveTimeout + 5000);
server.requestTimeout = 0;

server.listen(port, host, () => {
  process.stdout.write(`${JSON.stringify({ serviceId, status: "listening", host, port })}\n`);
});

for (const signal of ["SIGINT", "SIGTERM"]) {
  process.on(signal, () => {
    server.close(() => process.exit(0));
    setTimeout(() => process.exit(0), 5000).unref();
  });
}

function handleHttpRequest(request, response) {
  const url = incomingUrl(request);

  if (request.method === "GET" && url.pathname === "/health/live") {
    proxyHttpRequest(request, response, upstream, proxyTimeoutMs);
    return;
  }

  if (request.method === "GET" && (url.pathname === "/health" || url.pathname === "/edge/health")) {
    writeJson(response, healthPayload({ serviceId, role: "public-dev-ingress", details: proxyDetails() }));
    return;
  }

  if (request.method === "GET" && (url.pathname === "/status" || url.pathname === "/routes")) {
    writeJson(response, {
      serviceId,
      environment: "dev",
      status: "ok",
      upstream,
      routes: [{ pathPrefix: "/", upstream, mode: "http-proxy" }]
    });
    return;
  }

  if (request.method === "GET" && url.pathname === "/help") {
    writeJson(response, { serviceId, commands: ["GET /health", "GET /edge/health", "GET /health/live", "GET /status", "GET /routes"] });
    return;
  }

  if (isInternalRoute(url.pathname)) {
    writeJson(response, { error: { code: "not_found", message: "Internal HWLAB routes are not exposed through the public edge proxy." } }, 404);
    return;
  }

  proxyHttpRequest(request, response, upstream, proxyTimeoutMs);
}

function handleUpgrade(request, socket, head) {
  const url = incomingUrl(request);
  if (isInternalRoute(url.pathname)) {
    socket.end("HTTP/1.1 404 Not Found\r\ncontent-type: text/plain; charset=utf-8\r\nconnection: close\r\n\r\nnot found");
    return;
  }
  const target = new URL(url.pathname + url.search, upstream);
  const transport = target.protocol === "https:" ? httpsRequest : httpRequest;
  const edgeTraceId = newTraceId();
  let timedOut = false;
  const upstreamRequest = transport(target, {
    method: request.method || "GET",
    headers: websocketRequestHeaders(request.headers, target)
  });
  const timeout = setTimeout(() => {
    timedOut = true;
    upstreamRequest.destroy(new Error(`upstream websocket timed out after ${proxyTimeoutMs}ms`));
  }, proxyTimeoutMs);
  const finish = () => clearTimeout(timeout);
  upstreamRequest.on("upgrade", (upstreamResponse, upstreamSocket, upstreamHead) => {
    finish();
    logProxyEvent("ws.upstream_open", { edgeTraceId, path: url.pathname, upstreamUrl: redactUrlToken(target.toString()), statusCode: upstreamResponse.statusCode ?? 101 });
    socket.write(formatUpgradeResponse(upstreamResponse));
    if (upstreamHead?.length) socket.write(upstreamHead);
    if (head?.length) upstreamSocket.write(head);
    upstreamSocket.on("error", (error) => logProxyEvent("ws.upstream_socket_error", { edgeTraceId, path: url.pathname, message: error.message }));
    socket.on("error", (error) => logProxyEvent("ws.client_socket_error", { edgeTraceId, path: url.pathname, message: error.message }));
    upstreamSocket.pipe(socket);
    socket.pipe(upstreamSocket);
  });
  upstreamRequest.on("response", (upstreamResponse) => {
    finish();
    socket.write(formatHttpResponse(upstreamResponse));
    upstreamResponse.pipe(socket);
  });
  upstreamRequest.on("error", (error) => {
    finish();
    logProxyEvent("ws.upstream_error", { edgeTraceId, path: url.pathname, timedOut, message: error.message });
    socket.end("HTTP/1.1 502 Bad Gateway\r\ncontent-type: application/json; charset=utf-8\r\nconnection: close\r\n\r\n" + JSON.stringify(proxyFailurePayload({ timedOut, timeoutMs: proxyTimeoutMs, upstreamUrl: upstream, edgeTraceId })) + "\n");
  });
  socket.on("error", () => upstreamRequest.destroy());
  upstreamRequest.end();
}

function proxyHttpRequest(request, response, upstreamUrl, timeoutMs) {
  const target = new URL(incomingUrl(request).pathname + incomingUrl(request).search, upstreamUrl);
  const transport = target.protocol === "https:" ? httpsRequest : httpRequest;
  const edgeTraceId = newTraceId();
  let timedOut = false;
  let upstreamResponded = false;
  const upstreamRequest = transport(target, {
    method: request.method || "GET",
    headers: nodeRequestHeaders(request.headers, target)
  }, (upstreamResponse) => {
    upstreamResponded = true;
    clearTimeout(timeout);
    response.writeHead(upstreamResponse.statusCode ?? 502, responseHeadersFromNode(upstreamResponse.headers));
    upstreamResponse.on("error", (error) => response.destroy(error));
    upstreamResponse.pipe(response);
  });
  const timeout = setTimeout(() => {
    timedOut = true;
    upstreamRequest.destroy(new Error(`upstream timed out after ${timeoutMs}ms`));
  }, timeoutMs);
  upstreamRequest.on("error", (error) => {
    clearTimeout(timeout);
    logProxyEvent("http.upstream_error", { edgeTraceId, path: incomingUrl(request).pathname, timedOut, upstreamResponded, message: error.message });
    if (response.headersSent) {
      response.destroy(error);
      return;
    }
    writeJson(response, proxyFailurePayload({ timedOut, timeoutMs, upstreamUrl, edgeTraceId }), 502);
  });
  request.on("aborted", () => upstreamRequest.destroy(new Error("client request aborted")));
  request.on("error", (error) => upstreamRequest.destroy(error));
  request.pipe(upstreamRequest);
}

function proxyDetails() {
  return {
    upstream,
    timeoutMs: proxyTimeoutMs,
    idleTimeoutSeconds,
    mode: "dev-edge-proxy"
  };
}

function parseTimeout(value, fallback, { min, max }) {
  const parsed = Number.parseInt(value || "", 10);
  if (!Number.isInteger(parsed)) return fallback;
  return Math.min(Math.max(parsed, min), max);
}

function incomingUrl(request) {
  const authority = request.headers.host || `${host}:${port}`;
  return new URL(request.url || "/", `http://${authority}`);
}

function nodeRequestHeaders(headers, target) {
  const next = {};
  for (const [key, value] of Object.entries(headers)) {
    if (value === undefined || isHopByHopHeader(key)) continue;
    next[key] = value;
  }
  next.host = target.host;
  return next;
}

function websocketRequestHeaders(headers, target) {
  const next = {};
  for (const [key, value] of Object.entries(headers)) {
    if (value === undefined || key.toLowerCase() === "host") continue;
    next[key] = value;
  }
  next.host = target.host;
  return next;
}

function responseHeadersFromNode(headers) {
  const next = {};
  for (const [key, value] of Object.entries(headers)) {
    if (value === undefined || isHopByHopHeader(key)) continue;
    next[key] = value;
  }
  next["cache-control"] ??= "no-store";
  return next;
}

function formatUpgradeResponse(response) {
  return formatStatusAndHeaders(response.statusCode ?? 101, response.statusMessage || "Switching Protocols", response.rawHeaders);
}

function formatHttpResponse(response) {
  return formatStatusAndHeaders(response.statusCode ?? 502, response.statusMessage || http.STATUS_CODES[response.statusCode ?? 502] || "Bad Gateway", response.rawHeaders);
}

function formatStatusAndHeaders(statusCode, statusMessage, rawHeaders) {
  const lines = [`HTTP/1.1 ${statusCode} ${statusMessage}`];
  for (let index = 0; index < rawHeaders.length; index += 2) {
    lines.push(`${rawHeaders[index]}: ${rawHeaders[index + 1]}`);
  }
  lines.push("", "");
  return lines.join("\r\n");
}

function isHopByHopHeader(key) {
  return ["connection", "keep-alive", "proxy-authenticate", "proxy-authorization", "te", "trailer", "transfer-encoding", "upgrade"].includes(key.toLowerCase());
}

function isInternalRoute(pathname) {
  return pathname === "/v1/internal" || pathname.startsWith("/v1/internal/");
}

function proxyFailurePayload({ timedOut, timeoutMs, upstreamUrl, edgeTraceId }) {
  const code = timedOut ? "proxy_timeout" : "upstream_unavailable";
  const userMessage = timedOut
    ? `Workbench 后端请求超过 ${timeoutMs}ms 未返回；请求已保留，可稍后重试。`
    : "Workbench 后端暂时不可用；请求已保留，可稍后重试。";
  return {
    status: "failed",
    error: {
      code,
      layer: "edge-proxy",
      category: timedOut ? "timeout" : "proxy",
      retryable: true,
      userMessage,
      message: userMessage,
      diagnostic: {
        contractVersion: "hwlab-error-diagnostic-v1",
        traceId: edgeTraceId,
        serviceId,
        layer: "edge-proxy",
        category: timedOut ? "timeout" : "proxy",
        code,
        httpStatus: 502,
        source: "edge-proxy",
        observedAt: new Date().toISOString(),
        valuesPrinted: false
      },
      blocker: { code, layer: "edge-proxy", retryable: true, summary: userMessage }
    },
    upstream: upstreamUrl,
    message: userMessage
  };
}

function writeJson(response, body, status = 200) {
  response.writeHead(status, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" });
  response.end(`${JSON.stringify(body, null, 2)}\n`);
}

function newTraceId() {
  return randomBytes(16).toString("hex");
}

function logProxyEvent(event, fields) {
  process.stderr.write(`${JSON.stringify({ event: `hwlab-edge-proxy.${event}`, observedAt: new Date().toISOString(), valuesPrinted: false, ...fields })}\n`);
}

function redactUrlToken(urlValue) {
  const parsed = new URL(urlValue);
  if (parsed.searchParams.has("token")) parsed.searchParams.set("token", "<redacted>");
  return parsed.toString();
}
