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

const serviceId = "hwlab-codex-api-responses-forwarder";
const defaultPort = 49280;
const port = parsePositiveInteger(process.env.HWLAB_CODE_AGENT_CODEX_API_FORWARDER_PORT || process.env.PORT, defaultPort);
const host = firstNonEmpty(process.env.HWLAB_CODE_AGENT_CODEX_API_FORWARDER_HOST, "127.0.0.1");
const upstreamBaseUrl = new URL(firstNonEmpty(process.env.HWLAB_CODE_AGENT_CODEX_API_UPSTREAM_BASE_URL, "https://hyueapi.com"));
const upstreamNoProxyEntries = noProxyEntriesForHost(upstreamBaseUrl.hostname);
const maxBodyBytes = parsePositiveInteger(process.env.HWLAB_CODE_AGENT_CODEX_API_FORWARDER_MAX_BODY_BYTES, 64 * 1024 * 1024);

const hopByHopHeaders = new Set([
  "connection",
  "keep-alive",
  "proxy-authenticate",
  "proxy-authorization",
  "te",
  "trailer",
  "transfer-encoding",
  "upgrade"
]);

ensureNoProxyEntries(upstreamNoProxyEntries);

const server = createServer(async (clientReq, clientRes) => {
  try {
    const url = new URL(clientReq.url || "/", "http://hwlab-codex-api-forwarder.local");
    if (url.pathname === "/health" || url.pathname === "/health/live" || url.pathname === "/health/liveliness") {
      sendJson(clientRes, 200, healthPayload("ok"));
      return;
    }
    if (url.pathname === "/health/readiness") {
      sendJson(clientRes, 200, healthPayload("ready"));
      return;
    }
    await proxyRequest(clientReq, clientRes, url);
  } catch (error) {
    sendJson(clientRes, 502, {
      error: {
        code: "codex_api_forwarder_failed",
        message: "Codex API Responses forwarder failed to proxy request",
        reason: error.message
      }
    });
  }
});

server.listen(port, host, () => {
  process.stdout.write(JSON.stringify({
    serviceId,
    status: "listening",
    host,
    port,
    upstreamHost: upstreamBaseUrl.hostname,
    noProxyRequired: upstreamNoProxyEntries
  }) + "\n");
});

async function proxyRequest(clientReq, clientRes, url) {
  const startedAt = Date.now();
  const targetUrl = targetUrlForRequest(url);
  const headers = filterRequestHeaders(clientReq.headers, targetUrl);
  const requestImpl = targetUrl.protocol === "https:" ? httpsRequest : httpRequest;

  const upstreamReq = requestImpl({
    protocol: targetUrl.protocol,
    hostname: targetUrl.hostname,
    port: targetUrl.port || (targetUrl.protocol === "https:" ? 443 : 80),
    method: clientReq.method,
    path: `${targetUrl.pathname}${targetUrl.search}`,
    headers
  }, (upstreamRes) => {
    const responseHeaders = filterResponseHeaders(upstreamRes.headers);
    clientRes.writeHead(upstreamRes.statusCode || 502, responseHeaders);
    let responseBytes = 0;
    upstreamRes.on("data", (chunk) => {
      responseBytes += chunk.length;
    });
    upstreamRes.on("end", () => {
      process.stderr.write(JSON.stringify({
        serviceId,
        method: clientReq.method,
        path: url.pathname,
        upstreamPath: targetUrl.pathname,
        status: upstreamRes.statusCode || null,
        responseBytes,
        elapsedMs: Date.now() - startedAt
      }) + "\n");
    });
    upstreamRes.pipe(clientRes);
  });

  upstreamReq.on("error", (error) => {
    if (!clientRes.headersSent) {
      sendJson(clientRes, 502, {
        error: {
          code: "codex_api_forwarder_upstream_error",
          message: "Codex API forwarder upstream request failed",
          reason: error.message
        }
      });
    } else {
      clientRes.end();
    }
  });

  await pipeRequestBody(clientReq, upstreamReq);
}

async function pipeRequestBody(clientReq, upstreamReq) {
  let bytes = 0;
  for await (const chunk of clientReq) {
    bytes += chunk.length;
    if (bytes > maxBodyBytes) {
      upstreamReq.destroy(new Error(`request body exceeded ${maxBodyBytes} bytes`));
      return;
    }
    if (!upstreamReq.write(chunk)) {
      await new Promise((resolve) => upstreamReq.once("drain", resolve));
    }
  }
  upstreamReq.end();
}

function targetUrlForRequest(url) {
  const target = new URL(upstreamBaseUrl.toString());
  target.pathname = upstreamPathFor(url.pathname);
  target.search = url.search;
  target.hash = "";
  return target;
}

function upstreamPathFor(inboundPath) {
  const basePath = upstreamBaseUrl.pathname.replace(/\/+$/u, "");
  const requestPath = normalizeCodexResponsesPath(inboundPath || "/");
  if (!basePath) return requestPath;
  if (basePath === "/v1" && requestPath.startsWith("/v1/")) return requestPath;
  if (basePath === "/v1" && requestPath === "/v1") return requestPath;
  if (basePath.endsWith("/responses") && (requestPath === "/responses" || requestPath === "/v1/responses")) return basePath;
  if (basePath.endsWith("/models") && (requestPath === "/models" || requestPath === "/v1/models")) return basePath;
  if (requestPath.startsWith(`${basePath}/`) || requestPath === basePath) return requestPath;
  return `${basePath}${requestPath}`;
}

function normalizeCodexResponsesPath(pathname) {
  if (upstreamBaseUrl.pathname.replace(/\/+$/u, "") === "/v1") return pathname;
  if (pathname === "/v1/responses") return "/responses";
  if (pathname === "/v1/models") return "/models";
  return pathname;
}

function filterRequestHeaders(headers, targetUrl) {
  const filtered = {};
  for (const [name, value] of Object.entries(headers)) {
    const lower = name.toLowerCase();
    if (hopByHopHeaders.has(lower) || lower === "host") continue;
    filtered[name] = value;
  }
  filtered.host = targetUrl.host;
  return filtered;
}

function filterResponseHeaders(headers) {
  const filtered = {};
  for (const [name, value] of Object.entries(headers)) {
    if (hopByHopHeaders.has(name.toLowerCase())) continue;
    filtered[name] = value;
  }
  return filtered;
}

function sendJson(res, status, payload) {
  const body = JSON.stringify(payload);
  res.writeHead(status, { "content-type": "application/json", "content-length": Buffer.byteLength(body) });
  res.end(body);
}

function healthPayload(status) {
  return {
    serviceId,
    status,
    listen: { host, port },
    upstream: {
      origin: upstreamBaseUrl.origin,
      host: upstreamBaseUrl.hostname,
      directNoProxyRequired: true,
      noProxyRequired: upstreamNoProxyEntries
    }
  };
}

function ensureNoProxyEntries(entries) {
  for (const name of ["NO_PROXY", "no_proxy"]) {
    process.env[name] = mergeNoProxyEntries(process.env[name], entries);
  }
}

function noProxyEntriesForHost(hostname) {
  const hostText = String(hostname || "").trim().toLowerCase();
  if (!hostText) return ["127.0.0.1", "localhost", "::1"];
  const entries = [hostText, "127.0.0.1", "localhost", "::1"];
  if (!/^\d+(?:\.\d+){3}$/u.test(hostText) && !hostText.includes(":")) entries.push(`.${hostText}`);
  return uniqueStrings(entries);
}

function mergeNoProxyEntries(current, additions = []) {
  return uniqueStrings([...String(current ?? "").split(/[,;\s]+/u), ...additions]).join(",");
}

function uniqueStrings(values) {
  const seen = new Set();
  const result = [];
  for (const value of values) {
    const text = String(value ?? "").trim();
    if (!text) continue;
    const key = text.toLowerCase();
    if (seen.has(key)) continue;
    seen.add(key);
    result.push(text);
  }
  return result;
}

function firstNonEmpty(...values) {
  for (const value of values) {
    const text = String(value ?? "").trim();
    if (text) return text;
  }
  return "";
}

function parsePositiveInteger(value, fallback) {
  const parsed = Number.parseInt(value || "", 10);
  return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
}
