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

// classes
import { SocketConnection } from './smartsocket.classes.socketconnection.js';
import {
  type ISocketFunctionCallDataRequest,
  SocketFunction,
  type ISocketFunctionCallDataResponse,
} from './smartsocket.classes.socketfunction.js';
import { SocketRequest } from './smartsocket.classes.socketrequest.js';
import { SocketServer } from './smartsocket.classes.socketserver.js';

import { logger } from './smartsocket.logging.js';

export interface ISmartsocketConstructorOptions {
  alias: string;
  port?: number;
}

export class Smartsocket {
  /**
   * a unique id to detect server restarts
   */
  public alias: string;
  public smartenv = new plugins.smartenv.Smartenv();
  public options: ISmartsocketConstructorOptions;
  public socketConnections = new plugins.lik.ObjectMap<SocketConnection>();
  public socketFunctions = new plugins.lik.ObjectMap<SocketFunction<any>>();
  public socketRequests = new plugins.lik.ObjectMap<SocketRequest<any>>();

  public eventSubject = new plugins.smartrx.rxjs.Subject<interfaces.TConnectionStatus>();

  private socketServer = new SocketServer(this);

  constructor(optionsArg: ISmartsocketConstructorOptions) {
    this.options = optionsArg;
    this.alias = plugins.isounique.uni(this.options.alias);
  }

  /**
   * Returns WebSocket hooks for integration with smartserve
   * Pass these hooks to SmartServe's websocket config
   */
  public getSmartserveWebSocketHooks(): pluginsTyped.ISmartserveWebSocketHooks {
    return this.socketServer.getSmartserveWebSocketHooks();
  }

  /**
   * starts smartsocket
   */
  public async start() {
    await this.socketServer.start();
  }

  /**
   * Handle a new WebSocket connection
   * Called by SocketServer when a new connection is established
   */
  public async handleNewConnection(socket: pluginsTyped.TWebSocket | pluginsTyped.IWebSocketLike) {
    const socketConnection: SocketConnection = new SocketConnection({
      alias: undefined,
      authenticated: false,
      side: 'server',
      smartsocketHost: this,
      socket: socket,
    });

    logger.log('info', 'Socket connected. Trying to authenticate...');
    this.socketConnections.add(socketConnection);

    // Handle disconnection
    const handleClose = () => {
      this.socketConnections.remove(socketConnection);
      socketConnection.eventSubject.next('disconnected');
    };

    (socket as pluginsTyped.IWebSocketLike).addEventListener('close', handleClose);
    (socket as pluginsTyped.IWebSocketLike).addEventListener('error', handleClose);

    try {
      await socketConnection.authenticate();
      await socketConnection.listenToFunctionRequests();

      // Signal that the server is ready
      socketConnection.sendMessage({
        type: 'serverReady',
        payload: {},
      });
    } catch (err) {
      logger.log('warn', `Authentication failed: ${err}`);
      this.socketConnections.remove(socketConnection);
    }
  }

  /**
   * stops smartsocket
   */
  public async stop() {
    await plugins.smartdelay.delayFor(1000);
    this.socketConnections.forEach((socketObjectArg: SocketConnection) => {
      if (socketObjectArg) {
        logger.log(
          'info',
          `disconnecting socket with >>alias ${socketObjectArg.alias} due to server stop...`
        );
        socketObjectArg.disconnect();
      }
    });
    this.socketConnections.wipe();

    // stop the corresponding server
    await this.socketServer.stop();
  }

  // communication

  /**
   * allows call to specific client.
   */
  public async clientCall<T extends plugins.typedrequestInterfaces.ITypedRequest>(
    functionNameArg: T['method'],
    dataArg: T['request'],
    targetSocketConnectionArg: SocketConnection
  ): Promise<T['response']> {
    const socketRequest = new SocketRequest<T>(this, {
      funcCallData: {
        funcDataArg: dataArg,
        funcName: functionNameArg,
      },
      originSocketConnection: targetSocketConnectionArg,
      shortId: plugins.isounique.uni(),
      side: 'requesting',
    });
    const response: ISocketFunctionCallDataResponse<T> = await socketRequest.dispatch();
    const result = response.funcDataArg;
    return result;
  }

  public addSocketFunction(socketFunction: SocketFunction<any>) {
    this.socketFunctions.add(socketFunction);
  }
}
