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.TWebSocket | pluginsTyped.IWebSocketLike;
}

/**
 * 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.TWebSocket | pluginsTyped.IWebSocketLike;

  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);
  }

  /**
   * Sends a message through the socket
   */
  public sendMessage(message: interfaces.ISocketMessage): void {
    if (this.socket.readyState === 1) { // WebSocket.OPEN
      this.socket.send(JSON.stringify(message));
    }
  }

  /**
   * Handles incoming messages
   */
  public handleMessage(messageData: interfaces.ISocketMessage): void {
    switch (messageData.type) {
      case 'function':
        this.handleFunctionCall(messageData as interfaces.ISocketMessage<interfaces.IFunctionCallPayload>);
        break;
      case 'functionResponse':
        this.handleFunctionResponse(messageData as interfaces.ISocketMessage<interfaces.IFunctionCallPayload>);
        break;
      case 'tagUpdate':
        this.handleTagUpdate(messageData as interfaces.ISocketMessage<interfaces.ITagUpdatePayload>);
        break;
      default:
        // Authentication messages are handled by the server/client classes
        break;
    }
  }

  private handleFunctionCall(messageData: interfaces.ISocketMessage<interfaces.IFunctionCallPayload>): void {
    if (!messageData.id) {
      logger.log('warn', 'received function call without request id');
      return;
    }
    const requestData: ISocketRequestDataObject<any> = {
      funcCallData: {
        funcName: messageData.payload.funcName,
        funcDataArg: messageData.payload.funcData,
      },
      shortId: messageData.id,
    };

    const referencedFunction: SocketFunction<any> =
      this.smartsocketRef.socketFunctions.findSync((socketFunctionArg) => {
        return socketFunctionArg.name === requestData.funcCallData.funcName;
      });

    if (referencedFunction) {
      const localSocketRequest = new SocketRequest(this.smartsocketRef, {
        side: 'responding',
        originSocketConnection: this,
        shortId: requestData.shortId,
        funcCallData: requestData.funcCallData,
      });
      localSocketRequest.createResponse();
    } else {
      logger.log('warn', `function ${requestData.funcCallData.funcName} not found or out of scope`);
    }
  }

  private handleFunctionResponse(messageData: interfaces.ISocketMessage<interfaces.IFunctionCallPayload>): void {
    if (!messageData.id) {
      logger.log('warn', 'received function response without request id');
      return;
    }
    const responseData: ISocketRequestDataObject<any> = {
      funcCallData: {
        funcName: messageData.payload.funcName,
        funcDataArg: messageData.payload.funcData,
      },
      shortId: messageData.id,
    };

    const targetSocketRequest = SocketRequest.getSocketRequestById(
      this.smartsocketRef,
      responseData.shortId
    );
    if (targetSocketRequest) {
      targetSocketRequest.handleResponse(responseData);
    }
  }

  private handleTagUpdate(messageData: interfaces.ISocketMessage<interfaces.ITagUpdatePayload>): void {
    const tagStoreArg = messageData.payload.tags as interfaces.TTagStore;
    if (!plugins.smartjson.deepEqualObjects(this.tagStore, tagStoreArg)) {
      this.tagStore = tagStoreArg;
      // Echo back to confirm
      this.sendMessage({
        type: 'tagUpdate',
        payload: { tags: this.tagStore },
      });
      this.tagStoreObservable.next(this.tagStore);
    }
    this.remoteTagStoreObservable.next(tagStoreArg);
  }

  /**
   * 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.sendMessage({
      type: 'tagUpdate',
      payload: { tags: this.tagStore },
    });
    await done.promise;
  }

  /**
   * Gets a tag by id
   */
  public getTagById(tagIdArg: interfaces.ITag['id']): interfaces.ITag | undefined {
    return this.tagStore[tagIdArg];
  }

  /**
   * Removes a tag from a connection
   */
  public removeTagById(tagIdArg: interfaces.ITag['id']): void {
    delete this.tagStore[tagIdArg];
    this.tagStoreObservable.next(this.tagStore);
    this.sendMessage({
      type: 'tagUpdate',
      payload: { tags: this.tagStore },
    });
  }

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

  /**
   * authenticate the socket (server side)
   */
  public authenticate(): Promise<SocketConnection> {
    const done = plugins.smartpromise.defer<SocketConnection>();

    // Set up message handler for authentication
    const messageHandler = (event: pluginsTyped.TMessageEvent) => {
      try {
        const data = typeof event.data === 'string' ? event.data : event.data.toString();
        const message: interfaces.ISocketMessage = JSON.parse(data);

        if (message.type === 'auth') {
          const authData = message.payload as interfaces.IAuthPayload;
          logger.log('info', 'received authentication data...');

          if (authData.alias) {
            this.alias = authData.alias;
            this.authenticated = true;

            // Send authentication response
            this.sendMessage({
              type: 'authResponse',
              payload: { success: true },
            });

            logger.log('ok', `socket with >>alias ${this.alias} is authenticated!`);
            done.resolve(this);
          } else {
            this.authenticated = false;
            this.sendMessage({
              type: 'authResponse',
              payload: { success: false, error: 'No alias provided' },
            });
            this.disconnect();
            done.reject('a socket tried to connect, but could not authenticate.');
          }
        }
      } catch (err) {
        logger.log('warn', `Failed to parse auth message: ${err instanceof Error ? err.message : String(err)}`);
      }
    };

    (this.socket as pluginsTyped.IWebSocketLike).addEventListener('message', messageHandler);

    // Request authentication
    const requestAuthPayload: interfaces.TAuthRequestMessage = {
      type: 'authRequest',
      payload: {
        serverAlias: (this.smartsocketRef as Smartsocket).alias,
      },
    };
    this.sendMessage(requestAuthPayload);

    return done.promise;
  }

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

  /**
   * listen to function requests
   */
  public listenToFunctionRequests() {
    const done = plugins.smartpromise.defer();
    if (this.authenticated) {
      // Set up message handler for all messages
      const messageHandler = (event: pluginsTyped.TMessageEvent) => {
        try {
          const data = typeof event.data === 'string' ? event.data : event.data.toString();
          const message: interfaces.ISocketMessage = JSON.parse(data);
          this.handleMessage(message);
        } catch (err) {
          logger.log('warn', `Failed to parse socket message: ${err instanceof Error ? err.message : String(err)}`);
        }
      };

      (this.socket as pluginsTyped.IWebSocketLike).addEventListener('message', messageHandler);

      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() {
    if (this.socket.readyState === 1 || this.socket.readyState === 0) {
      this.socket.close();
    }
    allSocketConnections.remove(this);
    this.updateStatus('disconnected');
  }

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