import * as http from "node:http";
import * as https from "node:https";
import * as net from "node:net";

import { NodeWebSocket, NodeWsClient, createWebSocketAccept } from "./node_ws_client.js";
import { type Message } from "./packets/message_common_pb.js";

export class NodeWsServer {
  private server: http.Server | https.Server | null = null;
  private readonly clients = new Set<NodeWsClient>();

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

  constructor(
    private readonly host: string,
    private readonly listenPort: number,
    private readonly path: string,
    private readonly newClient: (ws: NodeWebSocket) => NodeWsClient,
    private readonly tlsOptions?: https.ServerOptions,
  ) {}

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

  start(): Promise<void> {
    if (this.server !== null) {
      return Promise.resolve();
    }

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

      server.on("upgrade", (req, socket, head) => {
        this.handleUpgrade(req, socket as net.Socket, head);
      });
      server.once("error", onError);
      server.once("listening", onListening);
      server.listen(this.listenPort, this.host);
    });
  }

  async stop(): Promise<void> {
    if (this.server === null) {
      return;
    }

    const server = this.server;
    this.server = null;
    const clients = [...this.clients];
    this.clients.clear();
    const closeServer = new Promise<void>((resolve, reject) => {
      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: NodeWsClient): void {
    this.clients.delete(client);
  }

  private handleUpgrade(req: http.IncomingMessage, socket: net.Socket, head: Buffer): void {
    // upgrade socket도 client와 동일하게 Nagle 지연을 피하도록 no-delay를 켠다.
    socket.setNoDelay(true);

    const key = this.validateUpgrade(req);
    if (key === null) {
      socket.write("HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n");
      socket.destroy();
      return;
    }

    socket.write(
      [
        "HTTP/1.1 101 Switching Protocols",
        "Upgrade: websocket",
        "Connection: Upgrade",
        `Sec-WebSocket-Accept: ${createWebSocketAccept(key)}`,
        "\r\n",
      ].join("\r\n"),
    );
    this.handleConnection(new NodeWebSocket(socket, false, head));
  }

  private validateUpgrade(req: http.IncomingMessage): string | null {
    if (!this.pathMatches(req)) {
      return null;
    }

    const upgrade = String(req.headers.upgrade ?? "").toLowerCase();
    const connection = String(req.headers.connection ?? "").toLowerCase();
    const version = String(req.headers["sec-websocket-version"] ?? "");
    const keyHeader = req.headers["sec-websocket-key"];
    const key = Array.isArray(keyHeader) ? keyHeader[0] : keyHeader;

    if (upgrade !== "websocket" || !connection.split(",").map((part) => part.trim()).includes("upgrade")) {
      return null;
    }
    if (version !== "13" || typeof key !== "string" || key.length === 0) {
      return null;
    }

    return key;
  }

  private pathMatches(req: http.IncomingMessage): boolean {
    const requestUrl = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
    return requestUrl.pathname === this.path;
  }

  private handleConnection(ws: NodeWebSocket): void {
    const client = this.newClient(ws);
    this.clients.add(client);
    client.addDisconnectListener(() => {
      this.removeClient(client);
    });
    this.onClientConnected(client);
  }
}
