import { createServer } from "node:http";
import { readFile } from "node:fs/promises";
import { connect } from "node:net";
import { timingSafeEqual } from "node:crypto";

const allowedHosts = new Set([
  "api.cesium.com",
  "assets.ion.cesium.com",
  "dev.virtualearth.net",
  "ecn.t0.tiles.virtualearth.net",
  "ecn.t1.tiles.virtualearth.net",
  "ecn.t2.tiles.virtualearth.net",
  "ecn.t3.tiles.virtualearth.net",
]);
const config = await readConfig();

const server = createServer((request, response) => {
  if (!isAuthorized(request.headers["proxy-authorization"])) return writeJson(response, 401, { ok: false, error: "connector_unauthorized" }, { "proxy-authenticate": "Bearer" });
  if (request.method === "GET" && request.url === "/healthz") return writeJson(response, 200, { ok: true, service: "dc-amd-connector", mode: "restricted-connect", allowedHosts: allowedHosts.size });
  return writeJson(response, 405, { ok: false, error: "connect_only" });
});

server.on("connect", (request, clientSocket, head) => {
  if (!isAuthorized(request.headers["proxy-authorization"])) return rejectTunnel(clientSocket, 407, "Proxy Authentication Required", { "Proxy-Authenticate": "Bearer" });

  let target;
  try {
    target = parseConnectTarget(request.url || "");
  } catch (error) {
    log("warn", "connect_rejected", { reason: error.message });
    return rejectTunnel(clientSocket, 403, "Forbidden");
  }

  const upstream = connect({ host: target.host, port: target.port });
  let settled = false;
  const timeout = setTimeout(() => upstream.destroy(new Error("upstream_connect_timeout")), config.connectTimeoutMs);
  clientSocket.setTimeout(config.idleTimeoutMs, () => clientSocket.destroy());
  upstream.setTimeout(config.idleTimeoutMs, () => upstream.destroy());

  upstream.once("connect", () => {
    settled = true;
    clearTimeout(timeout);
    clientSocket.write("HTTP/1.1 200 Connection Established\r\nProxy-Agent: dc-amd-connector\r\n\r\n");
    if (head?.length) upstream.write(head);
    clientSocket.pipe(upstream);
    upstream.pipe(clientSocket);
    log("info", "connect_established", { host: target.host, port: target.port });
  });
  upstream.once("error", (error) => {
    clearTimeout(timeout);
    if (!settled) {
      log("warn", "connect_failed", { host: target.host, port: target.port, reason: sanitizeError(error) });
      rejectTunnel(clientSocket, 502, "Bad Gateway");
    }
  });
  clientSocket.once("error", () => upstream.destroy());
  clientSocket.once("close", () => upstream.destroy());
  upstream.once("close", () => clientSocket.destroy());
});

server.on("clientError", (_error, socket) => rejectTunnel(socket, 400, "Bad Request"));
server.listen(config.port, "0.0.0.0", () => log("info", "connector_started", { port: config.port, allowedHosts: allowedHosts.size }));

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

async function readConfig() {
  const port = parsePort(process.env.PORT, 8791);
  const tokenFile = String(process.env.AMD_CONNECTOR_ACCESS_TOKEN_FILE || "/run/dc-amd-secrets/connector-access").trim();
  const token = String(await readFile(tokenFile, "utf8")).trim();
  if (!/^[A-Za-z0-9_-]{48,256}$/.test(token)) throw new Error("connector_access_token_invalid");
  return {
    port,
    token: Buffer.from(token, "utf8"),
    connectTimeoutMs: parseDuration(process.env.AMD_CONNECTOR_CONNECT_TIMEOUT_SECONDS, 20),
    idleTimeoutMs: parseDuration(process.env.AMD_CONNECTOR_IDLE_TIMEOUT_SECONDS, 90),
  };
}

function parsePort(raw, fallback) {
  const value = Number(String(raw || fallback).trim());
  if (!Number.isInteger(value) || value < 1024 || value > 65535) throw new Error("connector_port_invalid");
  return value;
}

function parseDuration(raw, fallbackSeconds) {
  const seconds = Number(String(raw || fallbackSeconds).trim());
  if (!Number.isInteger(seconds) || seconds < 1 || seconds > 600) throw new Error("connector_timeout_invalid");
  return seconds * 1000;
}

function isAuthorized(rawHeader) {
  const match = /^Bearer\s+([A-Za-z0-9_-]{48,256})$/i.exec(String(rawHeader || "").trim());
  if (!match) return false;
  const candidate = Buffer.from(match[1], "utf8");
  return candidate.length === config.token.length && timingSafeEqual(candidate, config.token);
}

function parseConnectTarget(raw) {
  const match = /^([A-Za-z0-9.-]{1,253}):(443)$/.exec(String(raw || "").trim());
  if (!match) throw new Error("connect_target_invalid");
  const host = match[1].toLowerCase();
  if (!allowedHosts.has(host)) throw new Error("connect_target_not_allowed");
  return { host, port: Number(match[2]) };
}

function rejectTunnel(socket, status, message, headers = {}) {
  if (!socket || socket.destroyed) return;
  const extra = Object.entries(headers).map(([name, value]) => `${name}: ${value}\r\n`).join("");
  socket.end(`HTTP/1.1 ${status} ${message}\r\n${extra}Connection: close\r\nContent-Length: 0\r\n\r\n`);
}

function writeJson(response, status, body, headers = {}) {
  const payload = JSON.stringify(body);
  response.writeHead(status, {
    "content-type": "application/json; charset=utf-8",
    "content-length": Buffer.byteLength(payload),
    "cache-control": "no-store",
    ...headers,
  });
  response.end(payload);
}

function sanitizeError(error) {
  const message = String(error?.message || "connector_error");
  return message.replace(/[^A-Za-z0-9_.:-]/g, "_").slice(0, 120);
}

function log(level, event, fields = {}) {
  console.log(JSON.stringify({ ts: new Date().toISOString(), level, event, ...fields }));
}
