import { Emitter } from "../utils/emitter"
import type { AgentConfig, AgentEvent } from "./types"

type AgentState = "disconnected" | "connecting" | "connected"

export class Agent {
  #config: AgentConfig
  #state: AgentState = "disconnected"
  #ws?: WebSocket
  #audioBuffer: Uint8Array[] = []
  #chunkBuffer = new Uint8Array(0)
  #chunkSize = 8000

  public readonly events = new Emitter<AgentEvent>()
  public conversationId?: string

  constructor(config: AgentConfig) {
    this.#config = config
  }

  get isConnected(): boolean {
    return this.#state === "connected"
  }

  start = async (): Promise<void> => {
    if (this.#state !== "disconnected") {
      return
    }

    this.#state = "connecting"
    this.#audioBuffer = []
    this.conversationId = undefined

    const wsUrl = `wss://api.elevenlabs.io/v1/convai/conversation?agent_id=${this.#config.agentId}`
    this.#ws = new WebSocket(wsUrl, {
      headers: { "xi-api-key": this.#config.apiKey },
    })

    this.#ws.addEventListener("open", this.#handleOpen)
    this.#ws.addEventListener("message", this.#handleMessage)
    this.#ws.addEventListener("error", this.#handleError)
    this.#ws.addEventListener("close", this.#handleClose)
  }

  stop = async (): Promise<void> => {
    if (this.#state === "disconnected") {
      return
    }

    this.#ws?.close()
    this.#cleanup()
  }

  sendAudio = (chunk: Uint8Array): void => {
    if (this.#state === "disconnected") {
      return
    }

    if (this.#state === "connecting") {
      this.#audioBuffer.push(chunk)
      return
    }

    // Chunk audio to appropriate size
    const newBuffer = new Uint8Array(this.#chunkBuffer.length + chunk.length)
    newBuffer.set(this.#chunkBuffer)
    newBuffer.set(chunk, this.#chunkBuffer.length)
    this.#chunkBuffer = newBuffer

    while (this.#chunkBuffer.length >= this.#chunkSize) {
      const audioChunk = this.#chunkBuffer.slice(0, this.#chunkSize)
      this.#chunkBuffer = this.#chunkBuffer.slice(this.#chunkSize)

      const base64Audio = Buffer.from(audioChunk).toString("base64")
      this.#send({ user_audio_chunk: base64Audio })
    }
  }

  sendMessage = (text: string): void => {
    if (this.#state !== "connected") {
      return
    }

    this.#send({ type: "user_message", text })
  }

  sendContextUpdate = (text: string): void => {
    if (this.#state !== "connected") {
      return
    }

    this.#send({ type: "contextual_update", text })
  }

  #handleOpen = (): void => {
    this.#state = "connected"

    // Send conversation config if provided
    if (this.#config.conversationConfig) {
      this.#send({
        type: "conversation_initiation_client_data",
        conversation_config_override: this.#config.conversationConfig.agentConfig,
        custom_llm_extra_body: this.#config.conversationConfig.customLlmExtraBody,
        dynamic_variables: this.#config.conversationConfig.dynamicVariables,
      })
    }

    // Flush buffered audio
    for (const chunk of this.#audioBuffer) {
      const base64 = Buffer.from(chunk).toString("base64")
      this.#send({ user_audio_chunk: base64 })
    }
    this.#audioBuffer = []
  }

  #handleMessage = async (event: MessageEvent): Promise<void> => {
    try {
      const message = JSON.parse(event.data as string)

      if (message.type === "conversation_initiation_metadata") {
        const metadata = message.conversation_initiation_metadata_event
        this.conversationId = metadata.conversation_id
        this.events.emit({
          type: "connected",
          conversationId: metadata.conversation_id,
          audioFormat: {
            agent_output_audio_format: metadata.agent_output_audio_format,
            user_input_audio_format: metadata.user_input_audio_format,
          },
        })
      } else if (message.type === "user_transcript") {
        this.events.emit({
          type: "user_transcript",
          transcript: message.user_transcription_event.user_transcript,
        })
      } else if (message.type === "agent_response") {
        this.events.emit({
          type: "agent_response",
          response: message.agent_response_event.agent_response,
        })
      } else if (message.type === "agent_response_correction") {
        this.events.emit({
          type: "agent_response_correction",
          original: message.agent_response_correction_event.original_agent_response,
          corrected: message.agent_response_correction_event.corrected_agent_response,
        })
      } else if (message.type === "internal_tentative_agent_response") {
        this.events.emit({
          type: "tentative_agent_response",
          response: message.tentative_agent_response_internal_event.tentative_agent_response,
        })
      } else if (message.type === "audio") {
        this.events.emit({
          type: "audio",
          audioBase64: message.audio_event.audio_base_64,
          eventId: message.audio_event.event_id,
        })
      } else if (message.type === "interruption") {
        this.events.emit({
          type: "interruption",
          eventId: message.interruption_event.event_id,
        })
      } else if (message.type === "ping") {
        const eventId = message.ping_event.event_id
        this.#send({ type: "pong", event_id: eventId })
        this.events.emit({
          type: "ping",
          eventId,
          pingMs: message.ping_event.ping_ms,
        })
      } else if (message.type === "vad_score") {
        this.events.emit({
          type: "vad_score",
          score: message.vad_score_event.vad_score,
        })
      } else if (message.type === "client_tool_call") {
        await this.#handleToolCall(message.client_tool_call)
      }
    } catch (error) {
      this.events.emit({
        type: "error",
        error: error instanceof Error ? error : new Error(String(error)),
      })
    }
  }

  #handleToolCall = async (toolCall: {
    tool_name: string
    tool_call_id: string
    parameters: Record<string, unknown>
  }): Promise<void> => {
    const { tool_name, tool_call_id, parameters } = toolCall

    this.events.emit({
      type: "tool_call",
      name: tool_name,
      args: parameters,
      callId: tool_call_id,
    })

    const handler = this.#config.tools?.[tool_name]

    if (!handler) {
      const error = new Error(`Tool ${tool_name} not found`)
      this.events.emit({
        type: "tool_error",
        name: tool_name,
        error,
        callId: tool_call_id,
      })
      this.#send({
        type: "client_tool_result",
        tool_call_id,
        result: error.message,
        is_error: true,
      })
      return
    }

    try {
      const result = await handler(parameters)
      this.events.emit({
        type: "tool_result",
        name: tool_name,
        result,
        callId: tool_call_id,
      })
      this.#send({
        type: "client_tool_result",
        tool_call_id,
        result: typeof result === "string" ? result : JSON.stringify(result),
        is_error: false,
      })
    } catch (error) {
      const err = error instanceof Error ? error : new Error(String(error))
      this.events.emit({
        type: "tool_error",
        name: tool_name,
        error: err,
        callId: tool_call_id,
      })
      this.#send({
        type: "client_tool_result",
        tool_call_id,
        result: err.message,
        is_error: true,
      })
    }
  }

  #handleError = (error: Event): void => {
    this.events.emit({
      type: "error",
      error: new Error(`WebSocket error: ${error.type}`),
    })
  }

  #handleClose = (event: CloseEvent): void => {
    this.#cleanup()
    this.events.emit({ type: "disconnected" })
  }

  #cleanup = (): void => {
    this.#state = "disconnected"
    this.#audioBuffer = []
    this.#chunkBuffer = new Uint8Array(0)

    if (this.#ws) {
      this.#ws.removeEventListener("open", this.#handleOpen)
      this.#ws.removeEventListener("message", this.#handleMessage)
      this.#ws.removeEventListener("error", this.#handleError)
      this.#ws.removeEventListener("close", this.#handleClose)
      this.#ws = undefined
    }
  }

  #send = (data: object): void => {
    if (!this.#ws || this.#ws.readyState !== WebSocket.OPEN) {
      return
    }

    this.#ws.send(JSON.stringify(data))
  }
}
