import { Server } from "socket.io";
import { events, EventTypes } from "../globalEvents";
import { WorkerQueue } from "./queues";
import { reserveWorkerId } from "../../workert";

function emitGlobalEvent<K extends keyof EventTypes>(
  name: K,
  ...args: Parameters<EventTypes[K]>
) {
  events.emit(name, ...args);
}

export const startCommsServer = () => {
  const ioServer = new Server(8671);

  ioServer.on("connection", (socket) => {
    events.emit("worker:io:connection", { socket });

    socket.on(
      "requestId",
      async (
        queueType: WorkerQueue,
        callback: (workerId: string | null, error?: string) => void,
      ) => {
        let workerId: string;
        try {
          workerId = await reserveWorkerId(queueType);
        } catch (errorData) {
          events.emit("worker:error", {
            error: errorData,
            context: "failed to reserve PgBoss worker ID",
          });
          callback(null, "failed to reserve PgBoss worker ID");
          return;
        }

        const workerNamespace = ioServer.of(`/worker-${workerId}`);

        callback(workerId);

        workerNamespace.on("connection", (workerSocket) => {
          let workerLogs: {
            message: string;
            timestamp: string;
          }[] = [];
          workerSocket.on(
            "log",
            (data: { message: string; level?: string } | string) => {
              const message = typeof data === "string" ? data : data.message;
              const level = (
                typeof data === "object" ? (data.level ?? "INFO") : "INFO"
              ) as import("../globalEvents").JobLogLevel;
              workerLogs.push({
                message,
                timestamp: new Date().toISOString(),
              });

              events.emit("job:log", {
                message,
                level,
                timestamp: new Date().toISOString(),
                workerId,
                queueType: queueType,
              });
            },
          );
          workerSocket.on(
            "globalEvent",
            (
              name: keyof EventTypes,
              data: Parameters<EventTypes[keyof EventTypes]>[0],
            ) => {
              emitGlobalEvent(name, data as never);
            },
          );
          workerSocket.on(
            "workerError",
            (errorData: { error: unknown; context?: string }) => {
              events.emit("job:error", {
                ...errorData,
                workerId,
                queueType,
              });
            },
          );
        });
      },
    );

    socket.on("disconnect", () => {
      events.emit("worker:io:disconnect", { socket });
    });
  });
};

/**
 * 1. Worker connects to socket server
 * 2. Worker request woker ID from server
 * 3. Server generates worker ID and sends to worker
 * 4. Worker connects to ID Namespace with worker ID
 * 5. Server listens for messages on worker ID Namespace
 * 6. Worker sends message to server with real time updates on events being run.
 * 6a. Server can send messages to worker to trigger events or request status updates.
 * 6b. Worker can emit globalEvents through server to consolidate logging and trigger cross-worker events.
 * 7. Worker disconnects from socket server when process exits
 */
