import * as net from "node:net";
import * as tls from "node:tls";

import { BaseClient } from "./base_client.js";
import { type ParserMap } from "./communicator.js";
import { PacketBaseSchema, toBinary, type PacketBase } from "./packets/message_common_pb.js";

export const MAX_PACKET_SIZE = 64 << 20;

export class TcpClient extends BaseClient {
  private readonly socket: net.Socket;
  private readBuf = Buffer.alloc(0);
  private parseQueue = Promise.resolve();

  constructor(socket: net.Socket, intervalSec: number, waitSec: number, parserMap: ParserMap) {
    super(intervalSec, waitSec, async () => {
      if (!socket.destroyed) {
        socket.destroy();
      }
    });
    this.socket = socket;
    // Nagle's algorithm은 작은 write를 모아 보내며 delayed-ACK와 만나면 ~40ms 고정 지연을 만든다.
    // roundtrip 패턴에서는 즉시 flush가 유리하므로 no-delay를 명시한다.
    this.socket.setNoDelay(true);
    this.initBase(parserMap);

    socket.on("data", (chunk) => {
      socket.pause();
      this.parseQueue = this.parseQueue.then(async () => {
        try {
          await this.onData(chunk);
        } catch {
          // ignore error to keep the chain alive
        } finally {
          socket.resume();
        }
      });
    });
    socket.on("error", () => {
      void this.onDisconnected();
    });
    socket.on("close", () => {
      void this.onDisconnected();
    });
  }

  async writePacket(base: PacketBase): Promise<void> {
    const data = toBinary(PacketBaseSchema, base);
    if (data.byteLength > MAX_PACKET_SIZE) {
      throw new Error("packet exceeds maximum size");
    }

    const header = Buffer.allocUnsafe(4);
    header.writeUInt32BE(data.byteLength, 0);

    await new Promise<void>((resolve, reject) => {
      this.socket.write(Buffer.concat([header, Buffer.from(data)]), (err) => {
        if (err) {
          reject(err);
          return;
        }
        resolve();
      });
    });
  }

  private async onData(chunk: Buffer): Promise<void> {
    this.readBuf = Buffer.concat([this.readBuf, chunk]);
    while (this.readBuf.length >= 4) {
      const length = this.readBuf.readUInt32BE(0);
      if (length === 0) {
        this.readBuf = this.readBuf.subarray(4);
        continue;
      }
      if (length > MAX_PACKET_SIZE) {
        void this.onDisconnected();
        return;
      }
      if (this.readBuf.length < 4 + length) {
        return;
      }

      const packetBytes = this.readBuf.subarray(4, 4 + length);
      this.readBuf = this.readBuf.subarray(4 + length);

      // raw PacketBase frame을 communicator로 넘긴다. gateway 미설정 시 inline decode +
      // enqueue를 await해 기존 backpressure를 유지하고, gateway 설정 시 off-thread decode 경로로 제출된다.
      // decode 실패는 setFrameErrorHandler(initBase)에서 onDisconnected로 처리한다.
      await this.communicator.onReceivedFrame(packetBytes);
      void this.sendHeartbeat();
    }
  }
}

export async function connectTcp(
  host: string,
  port: number,
  intervalSec: number,
  waitSec: number,
  parserMap: ParserMap,
): Promise<TcpClient> {
  return new Promise<TcpClient>((resolve, reject) => {
    const socket = net.createConnection({ host, port });
    const onError = (err: Error) => {
      socket.off("connect", onConnect);
      reject(err);
    };
    const onConnect = () => {
      socket.off("error", onError);
      resolve(new TcpClient(socket, intervalSec, waitSec, parserMap));
    };
    socket.once("error", onError);
    socket.once("connect", onConnect);
  });
}

export async function connectTcpTls(
  host: string,
  port: number,
  tlsOptions: tls.ConnectionOptions,
  intervalSec: number,
  waitSec: number,
  parserMap: ParserMap,
): Promise<TcpClient> {
  return new Promise<TcpClient>((resolve, reject) => {
    const socket = tls.connect({ ...tlsOptions, host, port });
    const cleanup = () => {
      socket.off("secureConnect", onSecureConnect);
      socket.off("error", onError);
    };
    const onSecureConnect = () => {
      cleanup();
      resolve(new TcpClient(socket, intervalSec, waitSec, parserMap));
    };
    const onError = (err: Error) => {
      cleanup();
      reject(err);
    };

    socket.once("secureConnect", onSecureConnect);
    socket.once("error", onError);
  });
}
