/**
 * SpeachesSttProvider
 *
 * Speech-to-text provider using Speaches (faster-whisper backend).
 * Connects to the Speaches server via its OpenAI-compatible
 * `/v1/audio/transcriptions` endpoint using the OpenAI SDK.
 *
 * Issue #390
 */

import { Injectable, Inject, Logger } from "@nestjs/common";
import OpenAI from "openai";
import { toFile } from "openai";
import { speechConfig, type SpeechConfig } from "../speech.config";
import type { ISTTProvider } from "../interfaces/stt-provider.interface";
import type {
  TranscribeOptions,
  TranscriptionResult,
  TranscriptionSegment,
} from "../interfaces/speech-types";

/**
 * Derive file extension from a MIME type for use in the uploaded file name.
 */
function extensionFromMimeType(mimeType: string): string {
  const mapping: Record<string, string> = {
    "audio/wav": "wav",
    "audio/wave": "wav",
    "audio/x-wav": "wav",
    "audio/mp3": "mp3",
    "audio/mpeg": "mp3",
    "audio/mp4": "mp4",
    "audio/m4a": "m4a",
    "audio/ogg": "ogg",
    "audio/flac": "flac",
    "audio/webm": "webm",
    "audio/mpga": "mpga",
  };
  return mapping[mimeType] ?? "wav";
}

/**
 * STT provider backed by a Speaches (faster-whisper) server.
 *
 * Speaches exposes an OpenAI-compatible `/v1/audio/transcriptions` endpoint,
 * so we re-use the official OpenAI SDK with a custom `baseURL`.
 *
 * @example
 * ```typescript
 * const provider = new SpeachesSttProvider(speechConfig);
 * const result = await provider.transcribe(audioBuffer, { language: "en" });
 * console.log(result.text);
 * ```
 */
@Injectable()
export class SpeachesSttProvider implements ISTTProvider {
  readonly name = "speaches";

  private readonly logger = new Logger(SpeachesSttProvider.name);
  private readonly client: OpenAI;
  private readonly config: SpeechConfig;

  constructor(
    @Inject(speechConfig.KEY)
    config: SpeechConfig
  ) {
    this.config = config;

    this.client = new OpenAI({
      baseURL: config.stt.baseUrl,
      apiKey: "not-needed", // Speaches does not require an API key
    });

    this.logger.log(
      `Speaches STT provider initialized (endpoint: ${config.stt.baseUrl}, model: ${config.stt.model})`
    );
  }

  /**
   * Transcribe audio data to text using the Speaches server.
   *
   * Sends the audio buffer to the `/v1/audio/transcriptions` endpoint
   * with `response_format=verbose_json` to get segments and duration data.
   *
   * @param audio - Raw audio data as a Buffer
   * @param options - Optional transcription parameters (model, language, prompt, temperature)
   * @returns Transcription result with text, language, duration, and optional segments
   * @throws {Error} If transcription fails (connection error, API error, etc.)
   */
  async transcribe(audio: Buffer, options?: TranscribeOptions): Promise<TranscriptionResult> {
    const model = options?.model ?? this.config.stt.model;
    const language = options?.language ?? this.config.stt.language;
    const mimeType = options?.mimeType ?? "audio/wav";
    const extension = extensionFromMimeType(mimeType);

    try {
      const file = await toFile(audio, `audio.${extension}`, {
        type: mimeType,
      });

      const response = await this.client.audio.transcriptions.create({
        file,
        model,
        language,
        response_format: "verbose_json",
        ...(options?.prompt !== undefined ? { prompt: options.prompt } : {}),
        ...(options?.temperature !== undefined ? { temperature: options.temperature } : {}),
      });

      return this.mapResponse(response, language);
    } catch (error: unknown) {
      const message = error instanceof Error ? error.message : String(error);
      this.logger.error(`Transcription failed: ${message}`);
      throw new Error(`STT transcription failed: ${message}`);
    }
  }

  /**
   * Check if the Speaches server is healthy and reachable.
   *
   * Attempts to list models from the server. Returns true if the request
   * succeeds, false otherwise.
   *
   * @returns true if the Speaches server is reachable and ready
   */
  async isHealthy(): Promise<boolean> {
    try {
      await this.client.models.list();
      return true;
    } catch (error: unknown) {
      const message = error instanceof Error ? error.message : String(error);
      this.logger.warn(`Speaches health check failed: ${message}`);
      return false;
    }
  }

  /**
   * Map the OpenAI SDK transcription response to our TranscriptionResult type.
   *
   * Handles both verbose responses (with duration, segments) and simple
   * responses (text only).
   */
  private mapResponse(
    response: OpenAI.Audio.Transcriptions.TranscriptionVerbose | Record<string, unknown>,
    fallbackLanguage: string
  ): TranscriptionResult {
    const text = (response as { text: string }).text;
    const verboseResponse = response as {
      text: string;
      language?: string;
      duration?: number;
      segments?: {
        text: string;
        start: number;
        end: number;
      }[];
    };

    const result: TranscriptionResult = {
      text,
      language: verboseResponse.language ?? fallbackLanguage,
    };

    if (verboseResponse.duration !== undefined) {
      result.durationSeconds = verboseResponse.duration;
    }

    if (verboseResponse.segments !== undefined && Array.isArray(verboseResponse.segments)) {
      result.segments = verboseResponse.segments.map(
        (segment): TranscriptionSegment => ({
          text: segment.text,
          start: segment.start,
          end: segment.end,
        })
      );
    }

    return result;
  }
}
