import { DEFAULT_AGENT_URL } from "../lib/constants";
import { AgentEvents } from "../lib/enums/AgentEvents";
import type { AgentLiveSchema, DeepgramClientOptions, FunctionCallResponse } from "../lib/types";
import { AbstractLiveClient } from "./AbstractLiveClient";

export class AgentLiveClient extends AbstractLiveClient {
  public namespace: string = "agent";

  constructor(options: DeepgramClientOptions, endpoint: string = "/:version/agent/converse") {
    super(options);
    this.baseUrl = options.agent?.websocket?.options?.url ?? DEFAULT_AGENT_URL;

    this.connect({}, endpoint);
  }

  /**
   * Sets up the connection event handlers.
   * This method is responsible for handling the various events that can occur on the WebSocket connection, such as opening, closing, and receiving messages.
   * - When the connection is opened, it emits the `AgentEvents.Open` event.
   * - When the connection is closed, it emits the `AgentEvents.Close` event.
   * - When an error occurs on the connection, it emits the `AgentEvents.Error` event.
   * - When a message is received, it parses the message and emits the appropriate event based on the message type.
   */
  public setupConnection(): void {
    // Set up standard connection events (open, close, error) using abstracted method
    this.setupConnectionEvents({
      Open: AgentEvents.Open,
      Close: AgentEvents.Close,
      Error: AgentEvents.Error,
    });

    // Set up message handling specific to agent conversations
    if (this.conn) {
      this.conn.onmessage = (event: MessageEvent) => {
        this.handleMessage(event);
      };
    }
  }

  /**
   * Handles incoming messages from the WebSocket connection.
   * @param event - The MessageEvent object representing the received message.
   */
  protected handleMessage(event: MessageEvent): void {
    if (typeof event.data === "string") {
      try {
        const data = JSON.parse(event.data);
        this.handleTextMessage(data);
      } catch (error) {
        this.emit(AgentEvents.Error, {
          event,
          data:
            event.data?.toString().substring(0, 200) +
            (event.data?.toString().length > 200 ? "..." : ""),
          message: "Unable to parse `data` as JSON.",
          error,
          url: this.conn?.url,
          readyState: this.conn?.readyState,
        });
      }
    } else if (event.data instanceof Blob) {
      event.data.arrayBuffer().then((buffer) => {
        this.handleBinaryMessage(Buffer.from(buffer));
      });
    } else if (event.data instanceof ArrayBuffer) {
      this.handleBinaryMessage(Buffer.from(event.data));
    } else if (Buffer.isBuffer(event.data)) {
      this.handleBinaryMessage(event.data);
    } else {
      console.log("Received unknown data type", event.data);
      this.emit(AgentEvents.Error, {
        event,
        message: "Received unknown data type.",
        url: this.conn?.url,
        readyState: this.conn?.readyState,
        dataType: typeof event.data,
      });
    }
  }

  /**
   * Handles binary messages received from the WebSocket connection.
   * @param data - The binary data.
   */
  protected handleBinaryMessage(data: Buffer): void {
    this.emit(AgentEvents.Audio, data);
  }

  /**
   * Handles text messages received from the WebSocket connection.
   * @param data - The parsed JSON data.
   */
  protected handleTextMessage(data: any): void {
    if (data.type in AgentEvents) {
      this.emit(data.type, data);
    } else {
      this.emit(AgentEvents.Unhandled, data);
    }
  }

  /**
   * To be called with your model configuration BEFORE sending
   * any audio data.
   * @param options - The SettingsConfiguration object.
   */
  public configure(options: AgentLiveSchema): void {
    const string = JSON.stringify({
      type: "Settings",
      ...options,
    });
    this.send(string);
  }

  /**
   * Provide new system prompt to the LLM.
   * @param prompt - The system prompt to provide.
   */
  public updatePrompt(prompt: string): void {
    this.send(JSON.stringify({ type: "UpdatePrompt", prompt }));
  }

  /**
   * Change the speak model.
   * @param model - The new model to use.
   */
  public updateSpeak(speakConfig: Exclude<AgentLiveSchema["agent"]["speak"], undefined>): void {
    this.send(JSON.stringify({ type: "UpdateSpeak", speak: speakConfig }));
  }

  /**
   * Immediately trigger an agent message. If this message
   * is sent while the user is speaking, or while the server is in the
   * middle of sending audio, then the request will be ignored and an InjectionRefused
   * event will be emitted.
   * @example "Hold on while I look that up for you."
   * @example "Are you still on the line?"
   * @param content - The message to speak.
   */
  public injectAgentMessage(content: string): void {
    this.send(JSON.stringify({ type: "InjectAgentMessage", content }));
  }

  /**
   * Send a text-based message to the agent as if it came from the user.
   * This allows you to inject user messages into the conversation for the agent to respond to.
   * @example "Hello! Can you hear me?"
   * @example "What's the weather like today?"
   * @param content - The specific phrase or statement the agent should respond to.
   */
  public injectUserMessage(content: string): void {
    this.send(JSON.stringify({ type: "InjectUserMessage", content }));
  }

  /**
   * Respond to a function call request.
   * @param response  - The response to the function call request.
   */
  public functionCallResponse(response: FunctionCallResponse): void {
    this.send(JSON.stringify({ type: "FunctionCallResponse", ...response }));
  }

  /**
   * Send a keepalive to avoid closing the websocket while you
   * are not transmitting audio. This should be sent at least
   * every 8 seconds.
   */
  public keepAlive(): void {
    this.send(JSON.stringify({ type: "KeepAlive" }));
  }
}
