/**
 * Mistral Voxtral TTS Provider
 *
 * Generates speech audio using Mistral's Voxtral text-to-speech API.
 * Handles audio format conversion for telephony (mu-law 8kHz).
 *
 * API details:
 * - Endpoint: POST https://api.mistral.ai/v1/audio/speech
 * - Model: voxtral-4b-tts
 * - PCM output: float32, 24kHz, mono
 * - Max 4096 chars/request
 * - 20 preset voices
 *
 * @see https://docs.mistral.ai/capabilities/audio/
 */

const VOXTRAL_BASE_URL = "https://api.mistral.ai/v1/audio/speech";
const VOXTRAL_MAX_CHARS = 4096;

/**
 * Mistral Voxtral TTS configuration.
 */
export interface MistralTTSConfig {
  /** Mistral API key (uses MISTRAL_API_KEY env if not set) */
  apiKey?: string;
  /** TTS model (default: voxtral-4b-tts) */
  model?: string;
  /**
   * Voice to use. One of the 20 Voxtral preset voices.
   * Default: sophia
   */
  voice?: string;
  /** Speed multiplier (default: 1.0) */
  speed?: number;
}

/**
 * Mistral Voxtral TTS Provider for generating speech audio.
 */
export class MistralTTSProvider {
  private apiKey: string;
  private model: string;
  private voice: string;
  private speed: number;

  constructor(config: MistralTTSConfig = {}) {
    this.apiKey = config.apiKey || process.env.MISTRAL_API_KEY || "";
    this.model = config.model || "voxtral-4b-tts";
    this.voice = config.voice || "sophia";
    this.speed = config.speed ?? 1.0;

    if (!this.apiKey) {
      throw new Error("Mistral API key required (set MISTRAL_API_KEY or pass apiKey)");
    }
  }

  /**
   * Generate speech audio from text.
   * Returns raw PCM audio data (float32, 24kHz, mono).
   */
  async synthesize(text: string): Promise<Buffer> {
    if (text.length > VOXTRAL_MAX_CHARS) {
      throw new Error(`Text exceeds Voxtral limit of ${VOXTRAL_MAX_CHARS} characters`);
    }

    const response = await fetch(VOXTRAL_BASE_URL, {
      method: "POST",
      headers: {
        Authorization: `Bearer ${this.apiKey}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        model: this.model,
        input: text,
        voice: this.voice,
        speed: this.speed,
        output_format: "pcm",
        stream_audio: false,
      }),
    });

    if (!response.ok) {
      const error = await response.text();
      throw new Error(`Voxtral TTS failed: ${response.status} - ${error}`);
    }

    const arrayBuffer = await response.arrayBuffer();
    return Buffer.from(arrayBuffer);
  }

  /**
   * Generate speech and convert to mu-law format for Twilio.
   * Twilio Media Streams expect 8kHz mono mu-law audio.
   *
   * Pipeline: Voxtral float32 PCM 24kHz → int16 PCM 24kHz → int16 PCM 8kHz → mu-law 8kHz
   */
  async synthesizeForTwilio(text: string): Promise<Buffer> {
    // Get raw PCM from Voxtral (float32, 24kHz, mono)
    const pcmFloat32 = await this.synthesize(text);

    // Convert float32 to int16
    const pcm24k = float32ToInt16(pcmFloat32);

    // Resample from 24kHz to 8kHz
    const pcm8k = resample24kTo8k(pcm24k);

    // Encode to mu-law
    return pcmToMulaw(pcm8k);
  }
}

/**
 * Convert float32 PCM buffer to int16 PCM buffer.
 * Voxtral returns float32 samples in [-1.0, 1.0]; telephony codecs expect int16.
 */
export function float32ToInt16(input: Buffer): Buffer {
  const sampleCount = Math.floor(input.length / 4);
  const output = Buffer.alloc(sampleCount * 2);

  for (let i = 0; i < sampleCount; i++) {
    const floatSample = input.readFloatLE(i * 4);
    // Clamp to [-1.0, 1.0] then scale to int16 range
    const clamped = Math.max(-1.0, Math.min(1.0, floatSample));
    const int16Sample = Math.round(clamped * 32767);
    output.writeInt16LE(clamp16(int16Sample), i * 2);
  }

  return output;
}

/**
 * Resample 24kHz PCM to 8kHz using linear interpolation.
 * Input/output: 16-bit signed little-endian mono.
 */
function resample24kTo8k(input: Buffer): Buffer {
  const inputSamples = input.length / 2;
  const outputSamples = Math.floor(inputSamples / 3);
  const output = Buffer.alloc(outputSamples * 2);

  for (let i = 0; i < outputSamples; i++) {
    // Calculate position in input (3:1 ratio)
    const srcPos = i * 3;
    const srcIdx = srcPos * 2;

    if (srcIdx + 2 < input.length) {
      // Linear interpolation between samples
      const s0 = input.readInt16LE(srcIdx);
      const s1 = input.readInt16LE(srcIdx + 2);
      const frac = srcPos % 1 || 0;
      const sample = Math.round(s0 + frac * (s1 - s0));
      output.writeInt16LE(clamp16(sample), i * 2);
    } else {
      output.writeInt16LE(input.readInt16LE(srcIdx), i * 2);
    }
  }

  return output;
}

/**
 * Clamp value to 16-bit signed integer range.
 */
function clamp16(value: number): number {
  return Math.max(-32768, Math.min(32767, value));
}

/**
 * Convert 16-bit PCM to 8-bit mu-law.
 * Standard G.711 mu-law encoding for telephony.
 */
function pcmToMulaw(pcm: Buffer): Buffer {
  const samples = pcm.length / 2;
  const mulaw = Buffer.alloc(samples);

  for (let i = 0; i < samples; i++) {
    const sample = pcm.readInt16LE(i * 2);
    mulaw[i] = linearToMulaw(sample);
  }

  return mulaw;
}

/**
 * Convert a single 16-bit linear sample to 8-bit mu-law.
 * Implements ITU-T G.711 mu-law encoding.
 */
function linearToMulaw(sample: number): number {
  const BIAS = 132;
  const CLIP = 32635;

  // Get sign bit
  const sign = sample < 0 ? 0x80 : 0;
  if (sample < 0) {
    sample = -sample;
  }

  // Clip to prevent overflow
  if (sample > CLIP) {
    sample = CLIP;
  }

  // Add bias and find segment
  sample += BIAS;
  let exponent = 7;
  for (let expMask = 0x4000; (sample & expMask) === 0 && exponent > 0; exponent--, expMask >>= 1) {
    // Find the segment (exponent)
  }

  // Extract mantissa bits
  const mantissa = (sample >> (exponent + 3)) & 0x0f;

  // Combine into mu-law byte (inverted for transmission)
  return ~(sign | (exponent << 4) | mantissa) & 0xff;
}

/**
 * Convert 8-bit mu-law to 16-bit linear PCM.
 * Useful for decoding incoming audio.
 */
export function mulawToLinear(mulaw: number): number {
  // mu-law is transmitted inverted
  mulaw = ~mulaw & 0xff;

  const sign = mulaw & 0x80;
  const exponent = (mulaw >> 4) & 0x07;
  const mantissa = mulaw & 0x0f;

  let sample = ((mantissa << 3) + 132) << exponent;
  sample -= 132;

  return sign ? -sample : sample;
}

/**
 * Chunk audio buffer into 20ms frames for streaming.
 * At 8kHz mono, 20ms = 160 samples = 160 bytes (mu-law).
 */
export function chunkAudio(audio: Buffer, chunkSize = 160): Generator<Buffer, void, unknown> {
  return (function* () {
    for (let i = 0; i < audio.length; i += chunkSize) {
      yield audio.subarray(i, Math.min(i + chunkSize, audio.length));
    }
  })();
}
