// src/services/socket.ts
// Copyright (C) 2026 Robert Colbert <rob.colbert@openplatform.us>
// All Rights Reserved

import env from "../config/env.ts";
import http from "node:http";

import { DisconnectReason, ExtendedError, Socket, Server } from "socket.io";

import { GadgetSocket, SocketSessionType } from "../lib/socket-session.ts";
import { CodeSession } from "../lib/code-session.ts";
import { DroneSession } from "../lib/drone-session.ts";

import {
  ClientToServerEvents,
  IDroneRegistration,
  IIdeSession,
  ServerToClientEvents,
  SocketData,
} from "@gadget/api";

import DroneService from "./drone.ts";
import SessionService from "./session.ts";
import { DtpService } from "../lib/service.ts";

type CodeSessionMap = Map<string, CodeSession>;
type DroneSessionMap = Map<string, DroneSession>;
type ChatSessionCodeSessionMap = Map<string, CodeSession>;

class SocketService extends DtpService {
  private codeSessions: CodeSessionMap = new Map<string, CodeSession>();
  private droneSessions: DroneSessionMap = new Map<string, DroneSession>();
  private chatSessionIndex: ChatSessionCodeSessionMap = new Map<
    string,
    CodeSession
  >();
  private droneRegistrationIndex: DroneSessionMap = new Map<
    string,
    DroneSession
  >();
  private codeSessionUserIndex: CodeSessionMap = new Map<string, CodeSession>();

  /**
   * Tracks which code sessions (by socket ID) are monitoring which drones
   * (by registration ID). Used for broadcasting log/status events outside
   * the chat session routing path.
   */
  private droneMonitorIndex: Map<string, Set<string>> = new Map();

  private io?: Server<
    ClientToServerEvents,
    ServerToClientEvents,
    never,
    SocketData
  >;

  get name(): string {
    return "SocketService";
  }
  get slug(): string {
    return "svc:socket";
  }

  async start(): Promise<void> {
    this.log.info("started");
  }

  async stop(): Promise<void> {
    this.log.info("stopped");
  }

  async listen(httpServer: http.Server): Promise<void> {
    /*
     * Create Socket.io server
     */
    this.io = new Server<
      ClientToServerEvents,
      ServerToClientEvents,
      never,
      SocketData
    >(httpServer, {
      maxHttpBufferSize: env.socket.maxHttpBufferSize,
      pingInterval: 25000,  // 25s between pings (matches default)
      pingTimeout: 60000,   // 60s before disconnect (generous; default is 20s)
      cors: {
        origin: "*",
        methods: ["GET", "POST"],
      },
    });
    this.io.use(this.onSocketAuth.bind(this));
    this.io.on("connection", this.onSocketConnection.bind(this));
    this.log.info("socket.io server initialized");
  }

  async onSocketAuth(
    socket: GadgetSocket,
    next: (err?: ExtendedError) => void,
  ) {
    const token = socket.handshake.auth.token;
    // this.log.debug("received socket authentication request", { token });
    if (!token) {
      this.log.warn("socket connection rejected: no token provided");
      return next(new Error("Authentication required"));
    }

    /*
     * Try first to validate as a User JWT session
     */
    try {
      const user = await SessionService.verifyJsonWebToken(token);

      const session: CodeSession = new CodeSession(socket, user);
      this.codeSessions.set(socket.id, session);
      this.codeSessionUserIndex.set(user._id, session);
      session.register();

      socket.data = { sessionType: SocketSessionType.Code };
      socket.on("disconnect", (reason: DisconnectReason, extra?: unknown) => {
        this.onSocketDisconnect(socket, reason, extra);
      });

      return next();
    } catch (cause) {
      const error = cause as Error;
      if (error.name !== "TokenVerifyError") {
        this.log.warn("socket connection rejected: invalid token", {
          error,
        });
        return next(new Error("Invalid authentication token"));
      }
      // fall through to next test
    }

    /*
     * If not a User JWT, try to validate as a Drone session
     */
    try {
      const registrationId = token;
      const registration = await DroneService.getById(registrationId);

      const droneSession: DroneSession = new DroneSession(socket, registration);
      this.droneSessions.set(socket.id, droneSession);
      this.droneRegistrationIndex.set(registration._id, droneSession);
      droneSession.register();

      socket.data = { sessionType: SocketSessionType.Drone };
      socket.on("disconnect", (reason: DisconnectReason, extra?: unknown) => {
        this.onSocketDisconnect(socket, reason, extra);
      });

      return next();
    } catch (error) {
      this.log.warn("socket connection rejected: invalid auth token", {
        error,
      });
      next(new Error("Invalid authentication token"));
    }
  }

  onSocketConnection(socket: Socket) {
    switch (socket.data.sessionType) {
      case SocketSessionType.Code:
        return this.onSocketConnectCode(socket);
      case SocketSessionType.Drone:
        return this.onSocketConnectDrone(socket);
      default:
        break;
    }
    this.log.error("invalid socket session type during connect");
  }

  onSocketConnectCode(socket: Socket) {
    const session = this.codeSessions.get(socket.id);
    if (!session) {
      this.log.warn("invalid code session during connect");
      socket.disconnect(true);
      return;
    }
    this.log.info("code socket connected", {
      id: socket.id,
      userId: session.user._id,
    });
  }

  onSocketConnectDrone(socket: Socket) {
    const session = this.droneSessions.get(socket.id);
    if (!session) {
      this.log.warn("invalid drone session during connect");
      socket.disconnect(true);
      return;
    }
    this.log.info("drone socket connected", {
      id: socket.id,
      registrationId: session.registration._id,
    });
  }

  onSocketDisconnect(
    socket: Socket,
    reason: DisconnectReason,
    extra?: unknown,
  ) {
    this.log.info("socket disconnect", { reason, extra });
    switch (socket.data.sessionType) {
      case SocketSessionType.Code:
        this.log.info("closing code socket session", { id: socket.id });
        const disconnectingCodeSession = this.codeSessions.get(socket.id);
        if (disconnectingCodeSession) {
          // If this session held a drone lock, release it on disconnect
          const droneRegId = disconnectingCodeSession.selectedDroneId;
          const chatSess = disconnectingCodeSession.activeChatSession;
          const proj = disconnectingCodeSession.activeProject;
          if (droneRegId && chatSess && proj) {
            try {
              const droneSession = this.droneRegistrationIndex.get(droneRegId);
              if (droneSession) {
                droneSession.socket.emit(
                  "releaseSessionLock",
                  droneSession.registration,
                  proj,
                  chatSess,
                  () => {
                    /* fire-and-forget */
                  },
                );
              }
            } catch (error) {
              this.log.error(
                "failed to release session lock on code disconnect",
                { error },
              );
            }
          }
          // Clean up user index
          this.codeSessionUserIndex.delete(
            disconnectingCodeSession.user._id,
          );
          // Clean up chat session index
          if (chatSess) {
            this.unregisterChatSession(chatSess._id);
          }
        }
        this.codeSessions.delete(socket.id);
        return;

      case SocketSessionType.Drone:
        this.log.info("closing drone socket session", { id: socket.id });
        const droneSession = this.droneSessions.get(socket.id);
        if (droneSession) {
          this.droneRegistrationIndex.delete(droneSession.registration._id);
        }
        this.droneSessions.delete(socket.id);
        return;

      default:
        break;
    }

    this.log.error("invalid session type in socket disconnect", {
      id: socket.id,
      data: socket.data,
    });
  }

  getCodeSession(ideSession: IIdeSession): CodeSession {
    const session = this.codeSessionUserIndex.get(ideSession._id);
    if (!session) {
      const error = new Error("code session not found");
      error.statusCode = 404;
      throw error;
    }
    return session;
  }

  getDroneSession(registration: IDroneRegistration): DroneSession {
    const session = this.droneRegistrationIndex.get(registration._id);
    if (!session) {
      const error = new Error("drone session not found");
      error.statusCode = 404;
      throw error;
    }
    return session;
  }

  getDroneSessionByRegistrationId(registrationId: string): DroneSession {
    const session = this.droneRegistrationIndex.get(registrationId);
    if (!session) {
      const error = new Error("drone session not found");
      error.statusCode = 404;
      throw error;
    }
    return session;
  }

  addDroneMonitor(registrationId: string, socketId: string): void {
    let monitors = this.droneMonitorIndex.get(registrationId);
    if (!monitors) {
      monitors = new Set();
      this.droneMonitorIndex.set(registrationId, monitors);
    }
    monitors.add(socketId);
  }

  removeDroneMonitor(registrationId: string, socketId: string): void {
    const monitors = this.droneMonitorIndex.get(registrationId);
    if (monitors) {
      monitors.delete(socketId);
      if (monitors.size === 0) {
        this.droneMonitorIndex.delete(registrationId);
      }
    }
  }

  broadcastToMonitors(
    event: string,
    registrationId: string,
    ...args: unknown[]
  ): void {
    const monitorSocketIds = this.droneMonitorIndex.get(registrationId);
    if (!monitorSocketIds || monitorSocketIds.size === 0) return;

    for (const socketId of monitorSocketIds) {
      const codeSession = this.codeSessions.get(socketId);
      if (!codeSession) {
        // Stale entry — clean up
        monitorSocketIds.delete(socketId);
        continue;
      }
      if (!codeSession.socket.connected) {
        monitorSocketIds.delete(socketId);
        continue;
      }
      (codeSession.socket as any).emit(event, ...args);
    }

    if (monitorSocketIds.size === 0) {
      this.droneMonitorIndex.delete(registrationId);
    }
  }

  /**
   * Registers a code session by its chat session ID for reverse lookup.
   */
  registerChatSession(chatSessionId: string, codeSession: CodeSession): void {
    this.chatSessionIndex.set(chatSessionId, codeSession);
  }

  /**
   * Gets a code session by its chat session ID.
   */
  getCodeSessionByChatSessionId(chatSessionId: string): CodeSession {
    const session = this.chatSessionIndex.get(chatSessionId);
    if (!session) {
      const error = new Error("code session not found for chat session");
      error.statusCode = 404;
      throw error;
    }

    // CRITICAL: Check if socket is still connected
    if (!session.socket.connected) {
      this.log.warn("code session socket disconnected, clearing stale reference", {
        chatSessionId,
        socketId: session.socket.id,
      });
      this.chatSessionIndex.delete(chatSessionId);
      const error = new Error("code session socket disconnected");
      error.statusCode = 404;
      throw error;
    }

    return session;
  }

  /**
   * Unregisters a code session by its chat session ID.
   */
  unregisterChatSession(chatSessionId: string): void {
    this.chatSessionIndex.delete(chatSessionId);
  }
}

export default new SocketService();
