import { createServer, type Server, type Socket } from "node:net";
import type {
  CoreEgressTcpCloseMessage,
  CoreEgressTcpDataMessage,
  CoreEgressTcpOpenedMessage,
  JsonValue,
  ProviderEgressTcpCloseMessage,
  ProviderEgressTcpDataMessage,
  ProviderEgressTcpOpenMessage,
} from "../../shared/src/index";

type Logger = (level: "debug" | "info" | "warn" | "error", message: string, data?: JsonValue) => void;

type EgressToCoreMessage = ProviderEgressTcpOpenMessage | ProviderEgressTcpDataMessage | ProviderEgressTcpCloseMessage;
type EgressFromCoreMessage = CoreEgressTcpOpenedMessage | CoreEgressTcpDataMessage | CoreEgressTcpCloseMessage;

interface ProviderEgressProxyOptions {
  providerId: string;
  listenHost: string;
  listenPort: number;
  sendToCore: (message: EgressToCoreMessage) => boolean;
  isCoreConnected: () => boolean;
  logger: Logger;
}

interface Tunnel {
  id: string;
  client: Socket;
  method: string;
  opened: boolean;
  pending: Buffer[];
  pendingBytes: number;
  closed: boolean;
  createdAt: number;
  lastActivityAt: number;
  openTimer: ReturnType<typeof setTimeout> | null;
  idleTimer: ReturnType<typeof setTimeout> | null;
}

export interface ProviderEgressProxyHandle {
  status: () => Record<string, JsonValue>;
  handleCoreMessage: (message: EgressFromCoreMessage) => boolean;
  closeAll: (error?: string) => void;
  close: () => void;
}

function nowIso(): string {
  return new Date().toISOString();
}

function safeConnectionId(): string {
  return `egress_${Date.now()}_${Math.random().toString(16).slice(2)}`;
}

function parseConnectTarget(target: string): { host: string; port: number } | null {
  const trimmed = target.trim();
  if (trimmed.startsWith("[")) {
    const end = trimmed.indexOf("]");
    if (end <= 1 || trimmed[end + 1] !== ":") return null;
    const port = Number(trimmed.slice(end + 2));
    return Number.isInteger(port) ? { host: trimmed.slice(1, end), port } : null;
  }
  const index = trimmed.lastIndexOf(":");
  if (index <= 0) return null;
  const port = Number(trimmed.slice(index + 1));
  return Number.isInteger(port) ? { host: trimmed.slice(0, index), port } : null;
}

function parseHttpProxyRequest(headerText: string): { method: string; target: string; version: string } | null {
  const line = headerText.split(/\r?\n/u)[0] ?? "";
  const match = /^([A-Z]+)\s+(\S+)\s+(HTTP\/\d(?:\.\d)?)$/u.exec(line);
  if (match === null) return null;
  return { method: match[1] ?? "", target: match[2] ?? "", version: match[3] ?? "HTTP/1.1" };
}

function httpRequestTarget(rawTarget: string): { host: string; port: number; rewrittenTarget: string } | null {
  try {
    const url = new URL(rawTarget);
    if (url.protocol !== "http:") return null;
    return {
      host: url.hostname,
      port: url.port.length > 0 ? Number(url.port) : 80,
      rewrittenTarget: `${url.pathname}${url.search}`,
    };
  } catch {
    return null;
  }
}

function rewriteAbsoluteHttpRequest(header: Buffer, rewrittenTarget: string): Buffer {
  const text = header.toString("latin1");
  const lines = text.split(/\r\n/u);
  const first = lines[0] ?? "";
  const match = /^([A-Z]+)\s+\S+\s+(HTTP\/\d(?:\.\d)?)$/u.exec(first);
  if (match !== null) lines[0] = `${match[1]} ${rewrittenTarget} ${match[2]}`;
  return Buffer.from(lines.join("\r\n"), "latin1");
}

function response(status: string, body: string, contentType = "text/plain; charset=utf-8"): string {
  return [
    `HTTP/1.1 ${status}`,
    `Content-Type: ${contentType}`,
    "Cache-Control: no-store",
    "Connection: close",
    `Content-Length: ${Buffer.byteLength(body)}`,
    "",
    body,
  ].join("\r\n");
}

function jsonResponse(value: JsonValue): string {
  return response("200 OK", `${JSON.stringify(value)}\n`, "application/json; charset=utf-8");
}

function proxyUrlFor(host: string, port: number): string {
  return `http://${host}:${port}`;
}

const tunnelOpenTimeoutMs = 15_000;
const tunnelIdleTimeoutMs = 600_000;
const maxPendingBytes = 4 * 1024 * 1024;

export function startProviderEgressProxy(options: ProviderEgressProxyOptions): ProviderEgressProxyHandle {
  const proxyUrl = proxyUrlFor(options.listenHost, options.listenPort);
  const tunnels = new Map<string, Tunnel>();
  let closed = false;

  const send = (message: EgressToCoreMessage): boolean => options.sendToCore(message);

  const status = (): Record<string, JsonValue> => {
    const now = Date.now();
    const tunnelList = Array.from(tunnels.values());
    const ages = tunnelList.map((tunnel) => now - tunnel.createdAt);
    return {
      enabled: true,
      providerId: options.providerId,
      connected: options.isCoreConnected(),
      proxyUrl,
      listenHost: options.listenHost,
      listenPort: options.listenPort,
      activeTunnels: tunnels.size,
      pendingTunnels: tunnelList.filter((tunnel) => !tunnel.opened).length,
      oldestTunnelAgeMs: ages.length > 0 ? Math.max(...ages) : 0,
      openTimeoutMs: tunnelOpenTimeoutMs,
      idleTimeoutMs: tunnelIdleTimeoutMs,
      maxPendingBytes,
      channel: "provider-gateway",
    };
  };

  const clearTunnelTimers = (tunnel: Tunnel): void => {
    if (tunnel.openTimer !== null) clearTimeout(tunnel.openTimer);
    if (tunnel.idleTimer !== null) clearTimeout(tunnel.idleTimer);
    tunnel.openTimer = null;
    tunnel.idleTimer = null;
  };

  const destroyTunnel = (tunnel: Tunnel, notifyCore: boolean, error?: string): void => {
    tunnels.delete(tunnel.id);
    tunnel.closed = true;
    clearTunnelTimers(tunnel);
    tunnel.pending.splice(0);
    tunnel.pendingBytes = 0;
    if (!tunnel.client.destroyed) tunnel.client.destroy();
    if (notifyCore) send({ type: "egress_tcp_close", providerId: options.providerId, connectionId: tunnel.id, at: nowIso() });
    if (error !== undefined) {
      options.logger("warn", "egress_proxy_tunnel_closed", {
        connectionId: tunnel.id,
        opened: tunnel.opened,
        ageMs: Date.now() - tunnel.createdAt,
        error,
      });
    }
  };

  const closeTunnel = (id: string, error?: string): void => {
    const tunnel = tunnels.get(id);
    if (tunnel === undefined) return;
    destroyTunnel(tunnel, true, error);
  };

  const refreshTunnelIdle = (tunnel: Tunnel): void => {
    if (tunnel.closed) return;
    tunnel.lastActivityAt = Date.now();
    if (tunnel.idleTimer !== null) clearTimeout(tunnel.idleTimer);
    tunnel.idleTimer = setTimeout(() => closeTunnel(tunnel.id, "egress proxy idle timeout"), tunnelIdleTimeoutMs);
  };

  const queuePendingChunk = (tunnel: Tunnel, chunk: Buffer): boolean => {
    if (tunnel.closed) return false;
    tunnel.pending.push(chunk);
    tunnel.pendingBytes += chunk.byteLength;
    refreshTunnelIdle(tunnel);
    if (tunnel.pendingBytes <= maxPendingBytes) return true;
    closeTunnel(tunnel.id, "egress proxy pending buffer exceeded");
    return false;
  };

  const handleCoreMessage = (message: EgressFromCoreMessage): boolean => {
    const tunnel = tunnels.get(message.connectionId);
    if (message.type === "egress_tcp_opened") {
      if (tunnel === undefined || tunnel.closed) return true;
      tunnel.opened = true;
      if (tunnel.openTimer !== null) {
        clearTimeout(tunnel.openTimer);
        tunnel.openTimer = null;
      }
      refreshTunnelIdle(tunnel);
      if (tunnel.method === "CONNECT") {
        tunnel.client.write("HTTP/1.1 200 Connection Established\r\nProxy-Agent: UniDesk-ProviderGateway\r\n\r\n");
      }
      for (const chunk of tunnel.pending.splice(0)) {
        send({ type: "egress_tcp_data", providerId: options.providerId, connectionId: tunnel.id, data: chunk.toString("base64"), encoding: "base64", at: nowIso() });
      }
      tunnel.pendingBytes = 0;
      return true;
    }
    if (message.type === "egress_tcp_data") {
      if (tunnel === undefined || tunnel.client.destroyed) return true;
      refreshTunnelIdle(tunnel);
      tunnel.client.write(Buffer.from(message.data, message.encoding === "base64" ? "base64" : "utf8"));
      return true;
    }
    if (message.type === "egress_tcp_close") {
      if (tunnel !== undefined) {
        destroyTunnel(tunnel, false, message.error);
      }
      if (message.error !== undefined && message.error.length > 0) {
        options.logger("warn", "egress_proxy_remote_close", { connectionId: message.connectionId, error: message.error });
      }
      return true;
    }
    return false;
  };

  const server: Server = createServer((client) => {
    let header = Buffer.alloc(0);
    let settled = false;
    const fail = (statusCode: string, text: string): void => {
      if (!client.destroyed) client.end(response(statusCode, text));
    };
    client.on("data", (chunk) => {
      const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
      if (settled) return;
      header = Buffer.concat([header, buffer]);
      const headerEnd = header.indexOf("\r\n\r\n");
      if (headerEnd < 0) {
        if (header.length > 64 * 1024) {
          settled = true;
          fail("431 Request Header Fields Too Large", "proxy request header too large\n");
        }
        return;
      }
      settled = true;
      const head = header.subarray(0, headerEnd + 4);
      const rest = header.subarray(headerEnd + 4);
      const parsed = parseHttpProxyRequest(head.toString("latin1"));
      if (parsed === null) {
        fail("400 Bad Request", "invalid proxy request\n");
        return;
      }
      if (parsed.method === "GET" && (parsed.target === "/health" || parsed.target === "/__unidesk/egress-proxy/health")) {
        client.end(jsonResponse({ ok: true, service: "provider-gateway-egress-proxy", ...status() }));
        return;
      }
      if (!options.isCoreConnected()) {
        fail("503 Service Unavailable", "provider-gateway core channel is not connected\n");
        return;
      }
      const id = safeConnectionId();
      let firstPayload: Buffer | null = null;
      let target: { host: string; port: number } | null = null;
      if (parsed.method === "CONNECT") {
        target = parseConnectTarget(parsed.target);
        firstPayload = rest.length > 0 ? rest : null;
      } else {
        const httpTarget = httpRequestTarget(parsed.target);
        if (httpTarget !== null) {
          target = { host: httpTarget.host, port: httpTarget.port };
          firstPayload = Buffer.concat([rewriteAbsoluteHttpRequest(head, httpTarget.rewrittenTarget), rest]);
        }
      }
      if (target === null || target.port <= 0 || target.port > 65_535) {
        fail("400 Bad Request", "unsupported proxy target\n");
        return;
      }
      const createdAt = Date.now();
      const tunnel: Tunnel = {
        id,
        client,
        method: parsed.method,
        opened: false,
        pending: [],
        pendingBytes: 0,
        closed: false,
        createdAt,
        lastActivityAt: createdAt,
        openTimer: null,
        idleTimer: null,
      };
      tunnels.set(id, tunnel);
      client.on("data", (nextChunk) => {
        const nextBuffer = Buffer.isBuffer(nextChunk) ? nextChunk : Buffer.from(nextChunk);
        if (!tunnel.opened) {
          queuePendingChunk(tunnel, nextBuffer);
          return;
        }
        refreshTunnelIdle(tunnel);
        send({ type: "egress_tcp_data", providerId: options.providerId, connectionId: id, data: nextBuffer.toString("base64"), encoding: "base64", at: nowIso() });
      });
      client.on("close", () => closeTunnel(id));
      client.on("error", (error) => closeTunnel(id, error.message));
      const opened = send({ type: "egress_tcp_open", providerId: options.providerId, connectionId: id, host: target.host, port: target.port, at: nowIso() });
      if (!opened) {
        tunnels.delete(id);
        tunnel.closed = true;
        clearTunnelTimers(tunnel);
        fail("503 Service Unavailable", "provider-gateway core channel is not connected\n");
        return;
      }
      tunnel.openTimer = setTimeout(() => closeTunnel(id, "egress proxy open timeout"), tunnelOpenTimeoutMs);
      refreshTunnelIdle(tunnel);
      if (firstPayload !== null) queuePendingChunk(tunnel, firstPayload);
    });
    client.on("error", () => undefined);
  });

  server.listen(options.listenPort, options.listenHost, () => {
    options.logger("info", "egress_proxy_listening", status());
  });
  server.on("error", (error) => {
    options.logger("error", "egress_proxy_listen_failed", { proxyUrl, error: error.message });
  });

  return {
    status,
    handleCoreMessage,
    closeAll: (error?: string) => {
      for (const id of Array.from(tunnels.keys())) closeTunnel(id, error);
    },
    close: () => {
      closed = true;
      for (const id of Array.from(tunnels.keys())) closeTunnel(id, "egress proxy stopping");
      server.close();
    },
  };
}
