interface Action {
  id: string;
  data: any;
}

/**
 * Represents a conversation session, handling WebSocket communication and event management.
 */
export class Conversation {
  private conversationId: string;
  private socket: WebSocket;
  private listeners: Map<string, Function[]>;
  private metadata: object | null;
  private local_id: Number;
  /**
   * Initializes a new Conversation instance.
   * @param conversationId - The unique identifier of the conversation.
   * @param socket - The WebSocket instance for communication.
   */
  constructor(conversationId: string, socket: WebSocket, metadata?: object) {
    this.conversationId = conversationId;
    this.socket = socket;
    this.listeners = new Map();
    this.metadata = metadata || null; // If metadata is provided, use it; otherwise, set to null.
    this.local_id = Math.floor(Math.random() * 10000); // Simple random id for local instance tracking

    this.socket.onmessage = this.handleMessage.bind(this);

  }

  private handleMessage(event: MessageEvent) {
    const message = JSON.parse(event.data);
    const eventType = message.event?.event_type;
    if (eventType && this.listeners.has(eventType)) {
      const payload = message.event.event_payload;
      this.listeners.get(eventType)?.forEach((callback) => callback(payload));
    }
  }
  public restartListeners() {
    // Listen to WebSocket messages and handle events.
    this.socket.onmessage = this.handleMessage.bind(this);

  }

  /**
   * Adds an event listener for a specific event type.
   * @param eventType - The type of event to listen for.
   * @param callback - The function to invoke when the event occurs.
   */
  private addListener(eventType: string, callback: Function): void {
    this.restartListeners();
    if (!this.listeners.has(eventType)) {
      this.listeners.set(eventType, []);
    }
    this.listeners.get(eventType)?.push(callback);
  }

  /**
   * Registers a listener for messages from the bot or live chat.
   * @param callback - The function to handle incoming messages.
   */
  public onMessage(callback: (message: string, type: string) => void): void {
    this.addListener('bot_message', (payload: any) => callback(payload.content, 'ai_agent'));
    this.addListener('livechat_message', (payload: any) => callback(payload.content, 'human_agent'));
  }

  /**
     * Registers a listener for receiving actions.
     * @param callback - The function to handle incoming action.
     */
  public onActionReceived(callback: (actions: [Action]) => void): void {
    this.addListener('action', (payload: any) => callback(payload.actions));
  }

  /**
   * Registers a listener for updates to the conversation.
   * @param callback - The function to handle conversation updates.
   */
  public onConversationUpdate(callback: (update: any) => void): void {
    this.addListener('conversation_update', (payload: any) => {
      callback({
        type: payload.type,
        conversationId: payload.conversation_id,
        data: payload.data,
      });
    });
  }

  /**
   * Registers a listener for error events.
   * @param callback - The function to handle errors.
   */
  public onError(callback: (error: any) => void): void {
    this.addListener('general_error', (payload: any) => {
      callback({
        conversationId: payload.conversation_id,
        errorCode: payload.error_code,
        errorDesc: payload.error_desc,
      });
    });
  }

  /**
   * Sends a message to the conversation.
   * @param content - The message content to send.
   * @returns A promise that resolves when the message is sent.
   */
  public async sendMessage(content: object | string): Promise<void> {
    // Check if content is a string and transform it into a default object
    if (typeof content === 'string') {
      content = { type: 'text', text: content };
    }

    return this.sendPayload('user_message', {
      type: 'message_create',
      client_msg_id: `unique-message-id-${Date.now()}`,
      conversation_id: this.conversationId,
      content,
    });
  }

  /**
  * Sets metadata for the conversation and listens for success confirmation.
  * @param metadata - An object containing the metadata to set.
  * @returns A promise that resolves when the metadata update is successful.
  */
  public async setMetadata(metadata: object): Promise<void> {
    if (typeof metadata !== 'object' || metadata === null) {
      throw new Error('Metadata must be a non-null object.');
    }

    return new Promise((resolve, reject) => {
      const metadataRequest = {
        action: 'sendMessage',
        event: {
          event_type: 'metadata',
          event_payload: {
            metadata,
            client_msg_id: `metadata-${Date.now()}`,
            conversation_id: this.conversationId,
          },
        },
      };

      // Send the metadata update request
      this.socket.send(JSON.stringify(metadataRequest));

      // Listener function for metadata update success
      const onMetadataSuccess = (payload: any) => {

        if (payload.conversation_id === this.conversationId) {
          this.removeListener('metadata_update_success', onMetadataSuccess);
          resolve(payload);
        }
      };

      // Add the success listener
      this.addListener('metadata_update_success', onMetadataSuccess);

      // Timeout for failure case
      setTimeout(() => {
        this.removeListener('metadata_update_success', onMetadataSuccess);
        reject(new Error('Timeout: No response for metadata update'));
      }, 15000);
    });
  }


  /**
   * Sends an action to the conversation.
   * @param actionId - The unique ID of the action to send.
   * @param data - Optional data associated with the action.
   * @returns A promise that resolves when the action is sent.
   */
  public async sendAction(actionId: string, data: object = {}): Promise<void> {
    return this.sendPayload('action', {
      type: 'normal',
      id: actionId,
      data,
      conversation_id: this.conversationId,
    });
  }

  /**
   * Requests the transcript of the conversation.
   * @returns A promise that resolves to the conversation transcript.
   */
  public async getTranscript(): Promise<object[]> {
    return new Promise((resolve, reject) => {
      const transcriptRequest = {
        action: 'sendMessage',
        event: {
          event_type: 'conversation_transcript_request',
          event_payload: {
            conversation_id: this.conversationId,
          },
        },
      };

      this.socket.send(JSON.stringify(transcriptRequest));

      const onMessage = (payload: any) => {
        if (payload.conversation_id === this.conversationId) {
          this.removeListener('conversation_transcript', onMessage);
          resolve(payload.transcript);
        }
      };
      this.addListener('conversation_transcript', onMessage);

      setTimeout(() => {
        this.removeListener('conversation_transcript', onMessage);
        reject(new Error('Timeout: No response for transcript request'));
      }, 10000);
    });
  }

  /**
 * Requests metadata for the conversation.
 * @returns A promise that resolves to the conversation metadata.
 */
  public async getMetadata(): Promise<object> {
    return new Promise((resolve, reject) => {
      const metadataRequest = {
        action: 'sendMessage',
        event: {
          event_type: 'metadata_request',
          event_payload: {
            conversation_id: this.conversationId,
          },
        },
      };
      this.socket.send(JSON.stringify(metadataRequest));

      const onMessage = (payload: any) => {

        if (payload.conversation_id === this.conversationId) {
          this.removeListener('conversation_metadata', onMessage);
          resolve(payload.content);
        }
      };

      this.addListener('conversation_metadata', onMessage);

      setTimeout(() => {
        this.removeListener('conversation_metadata', onMessage);
        reject(new Error('Timeout: No response for metadata request'));
      }, 10000);
    });
  }
  /**
    * Deletes this conversation.
    * @returns A promise that resolves when the conversation is deleted successfully.
    */
  public async delete(): Promise<void> {
    return new Promise((resolve, reject) => {

      try {
        const deleteRequest = {
          action: 'sendMessage',
          event: {
            event_type: 'delete_conversation',
            event_payload: { conversation_id: this.conversationId },
          },
        };
        this.socket.send(JSON.stringify(deleteRequest));

        const onDeleteSuccess = (event: MessageEvent) => {
          const message = JSON.parse(event.data);
          if (
            message.event?.event_type === 'delete_conversation_success' &&
            message.event.event_payload.conversation_id === this.conversationId
          ) {
            this.socket.removeEventListener('message', onDeleteSuccess);
            resolve();
          }
        };

        this.socket.addEventListener('message', onDeleteSuccess);

        setTimeout(() => {
          this.socket.removeEventListener('message', onDeleteSuccess);
          reject(new Error(`Timeout: No response for deleting conversation ${this.conversationId}`));
        }, 10000);
      } catch (error) {
        reject(error);
      }
    });
  }

  /**
   * Edits a message in the conversation.
   * @param messageId - The ID of the message to edit.
   * @param content - The new content for the message (object or string).
   * @returns A promise that resolves when the edit is confirmed by user_message_updated.
   */
  public async editMessage(messageId: string, content: object | string): Promise<void> {
    // If content is a string, wrap it in a default object
    if (typeof content === 'string') {
      content = { type: 'text', text: content };
    }

    return new Promise((resolve, reject) => {
      // Send the edit_message payload
      this.sendPayload('edit_message', {
        type: 'message_create',
        client_msg_id: `edit-message-id-${Date.now()}`,
        conversation_id: this.conversationId,
        message_id: messageId,
        content,
      });

      // Listener for message_edited_success event
      const onEditSuccess = (payload: any) => {
        console.log(payload);
        if (
          payload.conversation_id === this.conversationId &&
          payload.message_id === messageId
        ) {
          this.removeListener('message_edited_success', onEditSuccess);
          resolve();
        }
      };

      this.addListener('message_edited_success', onEditSuccess);

      // Timeout for failure case
      setTimeout(() => {
        this.removeListener('message_edited_success', onEditSuccess);
        reject(new Error('Timeout: No response for message edit'));
      }, 10000);
    });
  }

  /**
   * Sends a payload to the WebSocket.
   * @param eventType - The type of event being sent.
   * @param payload - The payload data to include with the event.
   * @returns A promise that resolves when the payload is sent.
   */
  private async sendPayload(eventType: string, payload: object): Promise<void> {
    return new Promise((resolve, reject) => {
      try {
        const message = {
          action: 'sendMessage',
          event: {
            event_type: eventType,
            event_payload: payload,
          },
        };
        this.socket.send(JSON.stringify(message));
        resolve();
      } catch (error) {
        reject(error);
      }
    });
  }

  /**
   * Removes a specific event listener.
   * @param eventType - The type of event.
   * @param callback - The callback function to remove.
   */
  private removeListener(eventType: string, callback: Function): void {
    const callbacks = this.listeners.get(eventType);
    if (callbacks) {
      this.listeners.set(
        eventType,
        callbacks.filter((cb) => cb !== callback)
      );
    }
  }

  /**
   * Gets the unique identifier of the conversation.
   * @returns The conversation ID.
   */
  public getConversationId(): string {
    return this.conversationId;
  }
}
