import { connect as connectTcp } from "node:net";
import type {
  CoreEgressTcpCloseMessage,
  CoreEgressTcpDataMessage,
  CoreEgressTcpOpenedMessage,
  ProviderEgressTcpCloseMessage,
  ProviderEgressTcpDataMessage,
  ProviderEgressTcpOpenMessage,
} from "../../shared/src/index";
import { ctx, logger } from "./context";
import type { EgressTcpConnection, ProviderSocket } from "./types";
import { wsSendJson } from "./http";

function egressTcpKey(providerId: string, connectionId: string): string {
  return `${providerId}:${connectionId}`;
}

function isValidEgressHost(host: string): boolean {
  return host.length > 0 && host.length <= 253 && !/[\s/\\:@\0]/u.test(host);
}

function isValidEgressPort(port: number): boolean {
  return Number.isInteger(port) && port > 0 && port <= 65_535;
}

const egressTcpConnectTimeoutMs = 15_000;
const egressTcpIdleTimeoutMs = 600_000;

function sendEgressClose(provider: ProviderSocket, connectionId: string, error?: string): void {
  const message: CoreEgressTcpCloseMessage = error === undefined
    ? { type: "egress_tcp_close", connectionId }
    : { type: "egress_tcp_close", connectionId, error };
  wsSendJson(provider, message);
}

function clearConnectionTimers(connection: EgressTcpConnection): void {
  if (connection.connectTimer !== null) clearTimeout(connection.connectTimer);
  if (connection.idleTimer !== null) clearTimeout(connection.idleTimer);
  connection.connectTimer = null;
  connection.idleTimer = null;
}

function closeEgressTcpConnection(providerId: string, connectionId: string, error?: string): void {
  const key = egressTcpKey(providerId, connectionId);
  const connection = ctx.activeEgressTcpConnections.get(key);
  if (connection === undefined) return;
  ctx.activeEgressTcpConnections.delete(key);
  clearConnectionTimers(connection);
  connection.socket.destroy();
  if (error !== undefined) {
    logger("warn", "egress_tcp_connection_closed", { providerId, connectionId, error });
    sendEgressClose(connection.provider, connectionId, error);
  }
}

function refreshConnectionIdle(connection: EgressTcpConnection): void {
  if (connection.idleTimer !== null) clearTimeout(connection.idleTimer);
  connection.idleTimer = setTimeout(() => {
    closeEgressTcpConnection(connection.providerId, connection.connectionId, "egress tcp idle timeout");
  }, egressTcpIdleTimeoutMs);
}

export function handleEgressTcpOpen(ws: ProviderSocket, message: ProviderEgressTcpOpenMessage): void {
  const host = message.host.trim();
  const port = Number(message.port);
  if (!isValidEgressHost(host) || !isValidEgressPort(port)) {
    sendEgressClose(ws, message.connectionId, "invalid egress target");
    return;
  }
  const key = egressTcpKey(message.providerId, message.connectionId);
  closeEgressTcpConnection(message.providerId, message.connectionId);
  const socket = connectTcp({ host, port });
  const connection: EgressTcpConnection = {
    providerId: message.providerId,
    connectionId: message.connectionId,
    socket,
    provider: ws,
    connectTimer: null,
    idleTimer: null,
  };
  ctx.activeEgressTcpConnections.set(key, connection);
  connection.connectTimer = setTimeout(() => {
    closeEgressTcpConnection(message.providerId, message.connectionId, "egress tcp connect timeout");
  }, egressTcpConnectTimeoutMs);
  refreshConnectionIdle(connection);
  socket.on("connect", () => {
    if (ctx.activeEgressTcpConnections.get(key) !== connection) return;
    if (connection.connectTimer !== null) {
      clearTimeout(connection.connectTimer);
      connection.connectTimer = null;
    }
    refreshConnectionIdle(connection);
    const opened: CoreEgressTcpOpenedMessage = { type: "egress_tcp_opened", connectionId: message.connectionId };
    wsSendJson(ws, opened);
  });
  socket.on("data", (chunk) => {
    if (ctx.activeEgressTcpConnections.get(key) !== connection) return;
    refreshConnectionIdle(connection);
    const data: CoreEgressTcpDataMessage = {
      type: "egress_tcp_data",
      connectionId: message.connectionId,
      data: chunk.toString("base64"),
      encoding: "base64",
    };
    wsSendJson(ws, data);
  });
  socket.on("close", () => {
    if (ctx.activeEgressTcpConnections.get(key) !== connection) return;
    ctx.activeEgressTcpConnections.delete(key);
    clearConnectionTimers(connection);
    sendEgressClose(ws, message.connectionId);
  });
  socket.on("error", (error) => {
    if (ctx.activeEgressTcpConnections.get(key) !== connection) return;
    ctx.activeEgressTcpConnections.delete(key);
    clearConnectionTimers(connection);
    sendEgressClose(ws, message.connectionId, error.message);
  });
}

export function handleEgressTcpData(message: ProviderEgressTcpDataMessage): void {
  const connection = ctx.activeEgressTcpConnections.get(egressTcpKey(message.providerId, message.connectionId));
  if (connection === undefined) return;
  refreshConnectionIdle(connection);
  connection.socket.write(Buffer.from(message.data, message.encoding === "base64" ? "base64" : "utf8"));
}

export function handleEgressTcpClose(message: ProviderEgressTcpCloseMessage): void {
  closeEgressTcpConnection(message.providerId, message.connectionId);
}

export function closeEgressTcpConnectionsForProvider(providerId: string): void {
  for (const [key, connection] of ctx.activeEgressTcpConnections) {
    if (connection.providerId !== providerId) continue;
    ctx.activeEgressTcpConnections.delete(key);
    clearConnectionTimers(connection);
    connection.socket.destroy();
  }
}

export function closeEgressTcpConnectionsForSocket(provider: ProviderSocket): void {
  for (const [key, connection] of ctx.activeEgressTcpConnections) {
    if (connection.provider !== provider) continue;
    ctx.activeEgressTcpConnections.delete(key);
    clearConnectionTimers(connection);
    connection.socket.destroy();
  }
}
