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

import { type Message } from "./packets/message_common_pb.js";
import { TcpClient } from "./tcp_client.js";

export class TcpServer {
  private readonly server: net.Server | tls.Server;
  private readonly clients = new Set<TcpClient>();
  private started = false;

  onClientConnected: (client: TcpClient) => void = () => {};

  constructor(
    private readonly host: string,
    private readonly listenPort: number,
    private readonly newClient: (socket: net.Socket) => TcpClient,
    tlsOptions?: tls.TlsOptions,
  ) {
    const handleSocket = (socket: net.Socket): void => {
      const client = this.newClient(socket);
      this.clients.add(client);
      client.addDisconnectListener(() => {
        this.removeClient(client);
      });
      this.onClientConnected(client);
    };

    this.server = tlsOptions ? tls.createServer(tlsOptions, handleSocket) : net.createServer(handleSocket);
  }

  get port(): number {
    const address = this.server.address();
    if (address !== null && typeof address === "object") {
      return address.port;
    }
    return this.listenPort;
  }

  start(): Promise<void> {
    if (this.started) {
      return Promise.resolve();
    }

    return new Promise<void>((resolve, reject) => {
      const onError = (err: Error) => {
        cleanup();
        reject(err);
      };
      const onListening = () => {
        cleanup();
        this.started = true;
        resolve();
      };
      const cleanup = () => {
        this.server.off("error", onError);
        this.server.off("listening", onListening);
      };

      this.server.once("error", onError);
      this.server.once("listening", onListening);
      this.server.listen(this.listenPort, this.host);
    });
  }

  async stop(): Promise<void> {
    if (!this.started) {
      return;
    }
    this.started = false;

    const clients = [...this.clients];
    this.clients.clear();
    const closeServer = new Promise<void>((resolve, reject) => {
      this.server.close((err) => {
        if (err) {
          reject(err);
          return;
        }
        resolve();
      });
    });

    await Promise.allSettled(clients.map(async (client) => client.close()));
    await closeServer;
  }

  async broadcast(msg: Message): Promise<void> {
    await Promise.all([...this.clients].map(async (client) => client.communicator.send(msg)));
  }

  private removeClient(client: TcpClient): void {
    this.clients.delete(client);
  }
}
