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

// import classes
import { Smartsocket } from './smartsocket.classes.smartsocket.js';
import { SocketFunction } from './smartsocket.classes.socketfunction.js';
import { SocketRequest, type ISocketRequestDataObject } from './smartsocket.classes.socketrequest.js';

// socket.io
import { SmartsocketClient } from './smartsocket.classes.smartsocketclient.js';
import { logger } from './smartsocket.logging.js';

// export interfaces

/**
 * defines is a SocketConnection is server or client side. Important for mesh setups.
 */
export type TSocketConnectionSide = 'server' | 'client';

/**
 * interface for constructor of class SocketConnection
 */
export interface ISocketConnectionConstructorOptions {
  alias: string;
  authenticated: boolean;
  side: TSocketConnectionSide;
  smartsocketHost: Smartsocket | SmartsocketClient;
  socket: pluginsTyped.socketIo.Socket | pluginsTyped.socketIoClient.Socket;
}

/**
 * interface for authentication data
 */
export interface ISocketConnectionAuthenticationObject {
  alias: string;
}

// export classes
export let allSocketConnections = new plugins.lik.ObjectMap<SocketConnection>();

/**
 * class SocketConnection represents a websocket connection
 */
export class SocketConnection {
  public alias: string;
  public side: TSocketConnectionSide;
  public authenticated: boolean = false;
  public smartsocketRef: Smartsocket | SmartsocketClient;
  public socket: pluginsTyped.socketIo.Socket | pluginsTyped.socketIoClient.Socket;

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

  private tagStore: interfaces.TTagStore = {};
  public tagStoreObservable = new plugins.smartrx.rxjs.Subject<interfaces.TTagStore>();
  public remoteTagStoreObservable = new plugins.smartrx.rxjs.Subject<interfaces.TTagStore>();

  constructor(optionsArg: ISocketConnectionConstructorOptions) {
    this.alias = optionsArg.alias;
    this.authenticated = optionsArg.authenticated;
    this.side = optionsArg.side;
    this.smartsocketRef = optionsArg.smartsocketHost;
    this.socket = optionsArg.socket;

    // standard behaviour that is always true
    allSocketConnections.add(this);

    // handle connection
    this.socket.on('connect', async () => {
      this.updateStatus('connected');
    });
    this.socket.on('disconnect', async () => {
      logger.log(
        'info',
        `SocketConnection with >alias ${this.alias} on >side ${this.side} disconnected`
      );
      await this.disconnect();
      allSocketConnections.remove(this);
      this.eventSubject.next('disconnected');
    });
  }

  /**
   * adds a tag to a connection
   */
  public async addTag(tagArg: interfaces.ITag) {
    const done = plugins.smartpromise.defer();
    this.tagStore[tagArg.id] = tagArg;
    this.tagStoreObservable.next(this.tagStore);
    const remoteSubscription = this.remoteTagStoreObservable.subscribe((remoteTagStore) => {
      if (!remoteTagStore[tagArg.id]) {
        return;
      }
      const localTagString = plugins.smartjson.stringify(tagArg);
      const remoteTagString = plugins.smartjson.stringify(remoteTagStore[tagArg.id]);
      if (localTagString === remoteTagString) {
        remoteSubscription.unsubscribe();
        done.resolve();
      }
    });
    this.socket.emit('updateTagStore', this.tagStore);
    await done.promise;
  }

  /**
   * gets a tag by id
   * @param tagIdArg
   */
  public async getTagById(tagIdArg: interfaces.ITag['id']) {
    return this.tagStore[tagIdArg];
  }

  /**
   * removes a tag from a connection
   */
  public async removeTagById(tagIdArg: interfaces.ITag['id']) {
    delete this.tagStore[tagIdArg];
    this.tagStoreObservable.next(this.tagStore);
    this.socket.emit('updateTagStore', this.tagStore);
  }

  // authenticating --------------------------

  /**
   * authenticate the socket
   */
  public authenticate() {
    const done = plugins.smartpromise.defer();
    this.socket.on('dataAuth', async (dataArg: ISocketConnectionAuthenticationObject) => {
      logger.log('info', 'received authentication data...');
      this.socket.removeAllListeners('dataAuth');
      if (dataArg.alias) {
        // TODO: authenticate password
        this.alias = dataArg.alias;
        this.authenticated = true;
        this.socket.emit('authenticated');
        logger.log('ok', `socket with >>alias ${this.alias} is authenticated!`);
        done.resolve(this);
      } else {
        this.authenticated = false;
        await this.disconnect();
        done.reject('a socket tried to connect, but could not authenticated.');
      }
    });
    const requestAuthPayload: interfaces.IRequestAuthPayload = {
      serverAlias: this.smartsocketRef.alias,
    };
    this.socket.emit('requestAuth', requestAuthPayload);
    return done.promise;
  }

  // listening -------------------------------

  /**
   * listen to function requests
   */
  public listenToFunctionRequests() {
    const done = plugins.smartpromise.defer();
    if (this.authenticated) {
      this.socket.on('function', (dataArg: ISocketRequestDataObject<any>) => {
        // check if requested function is available to the socket's scope
        // logger.log('info', 'function request received');
        const referencedFunction: SocketFunction<any> =
          this.smartsocketRef.socketFunctions.findSync((socketFunctionArg) => {
            return socketFunctionArg.name === dataArg.funcCallData.funcName;
          });
        if (referencedFunction) {
          // logger.log('ok', 'function in access scope');
          const localSocketRequest = new SocketRequest(this.smartsocketRef, {
            side: 'responding',
            originSocketConnection: this,
            shortId: dataArg.shortId,
            funcCallData: dataArg.funcCallData,
          });
          localSocketRequest.createResponse(); // takes care of creating response and sending it back
        } else {
          logger.log('warn', 'function not existent or out of access scope');
        }
      });
      this.socket.on('functionResponse', (dataArg: ISocketRequestDataObject<any>) => {
        // logger.log('info', `received response for request with id ${dataArg.shortId}`);
        const targetSocketRequest = SocketRequest.getSocketRequestById(
          this.smartsocketRef,
          dataArg.shortId
        );
        targetSocketRequest.handleResponse(dataArg);
      });

      this.socket.on('updateTagStore', async (tagStoreArg: interfaces.TTagStore) => {
        if (!plugins.smartjson.deepEqualObjects(this.tagStore, tagStoreArg)) {
          this.tagStore = tagStoreArg;
          this.socket.emit('updateTagStore', this.tagStore);
          this.tagStoreObservable.next(this.tagStore);
        }
        this.remoteTagStoreObservable.next(tagStoreArg);
      });

      logger.log(
        'info',
        `now listening to function requests for ${this.alias} on side ${this.side}`
      );
      done.resolve(this);
    } else {
      const errMessage = 'socket needs to be authenticated first';
      logger.log('error', errMessage);
      done.reject(errMessage);
    }
    return done.promise;
  }

  // disconnecting ----------------------
  public async disconnect() {
    this.socket.disconnect(true);
    this.updateStatus('disconnected');
  }

  private updateStatus(statusArg: interfaces.TConnectionStatus) {
    if (this.eventStatus !== statusArg) {
      this.eventSubject.next(statusArg);
    }
    this.eventStatus = statusArg;
  }
}
