import * as plugins from './smartsocket.plugins.js';
import * as pluginsTyped from './smartsocket.pluginstyped.js';

// used in case no other server is supplied
import { Smartsocket } from './smartsocket.classes.smartsocket.js';
import { logger } from './smartsocket.logging.js';

/**
 * class SocketServer
 * handles the WebSocket server in standalone mode, or provides hooks for smartserve integration
 */
export class SocketServer {
  private smartsocket: Smartsocket;
  private httpServer: pluginsTyped.http.Server | pluginsTyped.https.Server | null = null;
  private wsServer: pluginsTyped.ws.WebSocketServer | null = null;

  /**
   * whether httpServer is standalone (created by us)
   */
  private standaloneServer = false;

  constructor(smartSocketInstance: Smartsocket) {
    this.smartsocket = smartSocketInstance;
  }

  /**
   * Starts listening to incoming websocket connections (standalone mode).
   * If no port is specified, this is a no-op (hooks mode via smartserve).
   */
  public async start() {
    // If no port specified, we're in hooks mode - nothing to start
    if (!this.smartsocket.options.port) {
      return;
    }

    // Standalone mode - create our own HTTP server and WebSocket server
    const done = plugins.smartpromise.defer();
    const httpModule = await this.smartsocket.smartenv.getSafeNodeModule('http');
    const wsModule = await this.smartsocket.smartenv.getSafeNodeModule('ws');

    const httpServer = httpModule.createServer();
    this.httpServer = httpServer;
    this.standaloneServer = true;

    // Create WebSocket server attached to HTTP server
    const wsServer = new wsModule.WebSocketServer({ server: httpServer });
    this.wsServer = wsServer;

    wsServer.on('connection', (ws: pluginsTyped.ws.WebSocket) => {
      this.smartsocket.handleNewConnection(ws);
    });

    httpServer.listen(this.smartsocket.options.port, () => {
      logger.log(
        'success',
        `Server started in standalone mode on port ${this.smartsocket.options.port}`
      );
      done.resolve();
    });

    await done.promise;
  }

  /**
   * closes the server
   */
  public async stop() {
    const done = plugins.smartpromise.defer<void>();
    let resolved = false;

    if (this.wsServer) {
      // Close all WebSocket connections
      this.wsServer.clients.forEach((client) => {
        client.terminate();
      });
      this.wsServer.close();
      this.wsServer = null;
    }

    if (this.httpServer && this.standaloneServer) {
      const resolveOnce = () => {
        if (!resolved) {
          resolved = true;
          this.httpServer = null;
          this.standaloneServer = false;
          done.resolve();
        }
      };

      this.httpServer.close(() => {
        resolveOnce();
      });

      // Add a timeout in case close callback doesn't fire
      const timeoutId = setTimeout(() => {
        resolveOnce();
      }, 2000);

      // Ensure timeout doesn't keep process alive
      if (timeoutId.unref) {
        timeoutId.unref();
      }
    } else {
      done.resolve();
    }

    await done.promise;
  }

  /**
   * Returns WebSocket hooks for integration with smartserve.
   * Pass these hooks to SmartServe's websocket config.
   */
  public getSmartserveWebSocketHooks(): pluginsTyped.ISmartserveWebSocketHooks {
    return {
      onOpen: async (peer: pluginsTyped.ISmartserveWebSocketPeer) => {
        // Create a wrapper that adapts ISmartserveWebSocketPeer to WebSocket-like interface
        const wsLikeSocket = this.createWsLikeFromPeer(peer);
        await this.smartsocket.handleNewConnection(wsLikeSocket);
      },
      onMessage: async (peer: pluginsTyped.ISmartserveWebSocketPeer, message: pluginsTyped.ISmartserveWebSocketMessage) => {
        // Dispatch message to the SocketConnection via the adapter
        const adapter = peer.data.get('smartsocket_adapter') as any;
        if (adapter) {
          let textData: string | undefined;
          if (message.type === 'text' && message.text) {
            textData = message.text;
          } else if (message.type === 'binary' && message.data) {
            // Convert binary to text (Buffer/Uint8Array to string)
            textData = new TextDecoder().decode(message.data);
          }
          if (textData) {
            adapter.dispatchMessage(textData);
          }
        }
      },
      onClose: async (peer: pluginsTyped.ISmartserveWebSocketPeer, code: number, reason: string) => {
        // Dispatch close to the SocketConnection via the adapter
        const adapter = peer.data.get('smartsocket_adapter') as any;
        if (adapter) {
          adapter.dispatchClose();
        }
      },
      onError: async (peer: pluginsTyped.ISmartserveWebSocketPeer, error: Error) => {
        // Dispatch error to the SocketConnection via the adapter
        const adapter = peer.data.get('smartsocket_adapter') as any;
        if (adapter) {
          adapter.dispatchError();
        }
      },
    };
  }

  /**
   * Creates a WebSocket-like object from a smartserve peer
   * This allows our SocketConnection to work with both native WebSocket and smartserve peers
   */
  private createWsLikeFromPeer(peer: pluginsTyped.ISmartserveWebSocketPeer): pluginsTyped.IWebSocketLike {
    const messageListeners: Array<(event: pluginsTyped.TMessageEvent) => void> = [];
    const closeListeners: Array<() => void> = [];
    const errorListeners: Array<() => void> = [];

    // Store the adapter on the peer for message routing
    peer.data.set('smartsocket_adapter', {
      dispatchMessage: (data: string) => {
        messageListeners.forEach((listener) => {
          listener({ data });
        });
      },
      dispatchClose: () => {
        closeListeners.forEach((listener) => listener());
      },
      dispatchError: () => {
        errorListeners.forEach((listener) => listener());
      },
    });

    return {
      get readyState() { return peer.readyState; },
      send: (data: string) => peer.send(data),
      close: (code?: number, reason?: string) => peer.close(code, reason),
      addEventListener: (event: string, listener: any) => {
        if (event === 'message') {
          messageListeners.push(listener);
        } else if (event === 'close') {
          closeListeners.push(listener);
        } else if (event === 'error') {
          errorListeners.push(listener);
        }
      },
      removeEventListener: (event: string, listener: any) => {
        if (event === 'message') {
          const idx = messageListeners.indexOf(listener);
          if (idx >= 0) messageListeners.splice(idx, 1);
        } else if (event === 'close') {
          const idx = closeListeners.indexOf(listener);
          if (idx >= 0) closeListeners.splice(idx, 1);
        } else if (event === 'error') {
          const idx = errorListeners.indexOf(listener);
          if (idx >= 0) errorListeners.splice(idx, 1);
        }
      },
    };
  }
}
