/**
 * S2S Media Bridge
 *
 * Bridges telephony audio (e.g., Twilio media stream WebSocket) to an OpenAI
 * Realtime S2S session. Replaces the chained STT → LLM → TTS pipeline with
 * a single end-to-end speech-to-speech connection.
 *
 * Audio in:  Telephony mu-law → input_audio_buffer.append → OpenAI processes
 * Audio out: response.audio.delta → mu-law → callback → Telephony
 * Tools:     function_call → onToolCall → Minion tool → result → continue
 * Barge-in:  speech_started → cancel response + clear audio
 */

import { RealtimeS2SSession } from "./realtime-session.js";
import type { MappableTool } from "./tool-mapper.js";
import { executeRealtimeToolCall, mapMinionToolsToRealtime } from "./tool-mapper.js";
import type { RealtimeAudioFormat, RealtimeS2SSessionConfig } from "./types.js";

export interface S2SMediaBridgeConfig {
  /** OpenAI API key */
  apiKey: string;
  /** Realtime model (default: gpt-4o-realtime-preview) */
  model?: string;
  /** Voice for TTS output (default: verse) */
  voice?: string;
  /** System instructions for the voice agent */
  instructions: string;
  /** Input audio format (default: g711_ulaw) */
  inputAudioFormat?: RealtimeAudioFormat;
  /** Output audio format (default: g711_ulaw) */
  outputAudioFormat?: RealtimeAudioFormat;
  /** Minion tools to expose to the Realtime API */
  tools?: MappableTool[];
  /** Tool allowlist (if set, only these tool names are exposed) */
  toolAllowlist?: string[];
  /** Tool call timeout in ms (default: 10000) */
  toolTimeoutMs?: number;
  /** Turn detection settings */
  turnDetection?: {
    threshold?: number;
    silence_duration_ms?: number;
  };
}

export interface S2SMediaBridgeEvents {
  /** Called with output audio chunks (base64) to send back to telephony */
  onOutputAudio?: (base64Audio: string) => void;
  /** Called with transcripts for logging */
  onTranscript?: (speaker: "user" | "bot", text: string) => void;
  /** Called when user starts speaking (for barge-in clearing) */
  onSpeechStart?: () => void;
  /** Called when the bridge connects */
  onConnect?: () => void;
  /** Called when the bridge disconnects */
  onDisconnect?: () => void;
  /** Called on error */
  onError?: (error: Error) => void;
}

/**
 * Bridges a telephony audio stream to an OpenAI Realtime S2S session.
 *
 * Usage:
 *   const bridge = new S2SMediaBridge(config, events);
 *   await bridge.start();
 *   // Forward telephony audio:
 *   bridge.sendAudio(base64MuLawChunk);
 *   // Receive output audio via events.onOutputAudio callback
 *   // Cleanup:
 *   bridge.close();
 */
export class S2SMediaBridge {
  private session: RealtimeS2SSession | null = null;
  private config: S2SMediaBridgeConfig;
  private events: S2SMediaBridgeEvents;
  private tools: MappableTool[];
  private closed = false;

  constructor(config: S2SMediaBridgeConfig, events: S2SMediaBridgeEvents = {}) {
    this.config = config;
    this.events = events;
    this.tools = config.tools ?? [];
  }

  /**
   * Start the S2S session and connect to OpenAI Realtime API.
   */
  async start(): Promise<void> {
    if (this.closed) return;

    const realtimeTools = mapMinionToolsToRealtime(this.tools, this.config.toolAllowlist);

    const sessionConfig: RealtimeS2SSessionConfig = {
      apiKey: this.config.apiKey,
      model: this.config.model,
      voice: this.config.voice,
      instructions: this.config.instructions,
      tools: realtimeTools.length > 0 ? realtimeTools : undefined,
      inputAudioFormat: this.config.inputAudioFormat ?? "g711_ulaw",
      outputAudioFormat: this.config.outputAudioFormat ?? "g711_ulaw",
      turnDetection: this.config.turnDetection
        ? {
            threshold: this.config.turnDetection.threshold,
            silence_duration_ms: this.config.turnDetection.silence_duration_ms,
          }
        : undefined,
    };

    this.session = new RealtimeS2SSession(sessionConfig, {
      onAudioDelta: (base64Audio) => {
        this.events.onOutputAudio?.(base64Audio);
      },

      onInputTranscript: (text) => {
        if (text.trim()) {
          this.events.onTranscript?.("user", text);
        }
      },

      onTranscriptDone: (fullText) => {
        if (fullText.trim()) {
          this.events.onTranscript?.("bot", fullText);
        }
      },

      onToolCall: async (callId, name, args) => {
        console.log(`[S2SBridge] Tool call: ${name}(${args.slice(0, 100)}...)`);
        return executeRealtimeToolCall(name, args, this.tools, this.config.toolTimeoutMs ?? 10000);
      },

      onSpeechStarted: () => {
        this.events.onSpeechStart?.();
      },

      onSessionCreated: () => {
        console.log("[S2SBridge] Session created");
        this.events.onConnect?.();
      },

      onError: (error) => {
        console.error("[S2SBridge] Error:", error.message);
        this.events.onError?.(error);
      },
    });

    try {
      await this.session.connect();
    } catch (error) {
      console.error("[S2SBridge] Failed to connect:", error);
      this.events.onError?.(error instanceof Error ? error : new Error(String(error)));
      throw error;
    }
  }

  /**
   * Forward telephony audio to the Realtime API.
   * @param base64Audio Base64-encoded audio in the configured input format
   */
  sendAudio(base64Audio: string): void {
    this.session?.sendAudio(base64Audio);
  }

  /**
   * Update the session instructions dynamically.
   */
  updateInstructions(instructions: string): void {
    this.session?.updateSession({ instructions });
  }

  /**
   * Check if the bridge is connected.
   */
  isConnected(): boolean {
    return this.session?.isConnected() ?? false;
  }

  /**
   * Close the bridge and disconnect.
   */
  close(): void {
    this.closed = true;
    if (this.session) {
      this.session.close();
      this.session = null;
    }
    this.events.onDisconnect?.();
  }
}
