/**
 * Base TTS Provider
 *
 * Abstract base class implementing common OpenAI-compatible TTS logic.
 * All concrete TTS providers (Kokoro, Chatterbox, Piper) extend this class.
 *
 * Uses the OpenAI SDK with a configurable baseURL to communicate with
 * OpenAI-compatible speech synthesis endpoints.
 *
 * Issue #391
 */

import { Logger } from "@nestjs/common";
import OpenAI from "openai";
import type { ITTSProvider } from "../interfaces/tts-provider.interface";
import type {
  SpeechTier,
  SynthesizeOptions,
  SynthesisResult,
  VoiceInfo,
  AudioFormat,
} from "../interfaces/speech-types";

/** Default TTS model identifier used for OpenAI-compatible APIs */
const DEFAULT_MODEL = "tts-1";

/** Default voice when none is configured */
const DEFAULT_VOICE = "alloy";

/** Default audio format */
const DEFAULT_FORMAT: AudioFormat = "mp3";

/** Default speech speed multiplier */
const DEFAULT_SPEED = 1.0;

/** Health check timeout in milliseconds */
const HEALTH_CHECK_TIMEOUT_MS = 5000;

/**
 * Abstract base class for OpenAI-compatible TTS providers.
 *
 * Provides common logic for:
 * - Synthesizing text to audio via OpenAI SDK's audio.speech.create()
 * - Listing available voices (with a default implementation)
 * - Health checking the TTS endpoint
 *
 * Subclasses must set `name` and `tier` properties and may override
 * `listVoices()` to provide provider-specific voice lists.
 *
 * @example
 * ```typescript
 * class KokoroProvider extends BaseTTSProvider {
 *   readonly name = "kokoro";
 *   readonly tier: SpeechTier = "default";
 *
 *   constructor(baseURL: string) {
 *     super(baseURL, "af_heart", "mp3");
 *   }
 * }
 * ```
 */
export abstract class BaseTTSProvider implements ITTSProvider {
  abstract readonly name: string;
  abstract readonly tier: SpeechTier;

  protected readonly logger: Logger;
  protected readonly client: OpenAI;
  protected readonly baseURL: string;
  protected readonly defaultVoice: string;
  protected readonly defaultFormat: AudioFormat;

  /**
   * Create a new BaseTTSProvider.
   *
   * @param baseURL - The base URL for the OpenAI-compatible TTS endpoint
   * @param defaultVoice - Default voice ID to use when none is specified in options
   * @param defaultFormat - Default audio format to use when none is specified in options
   */
  constructor(
    baseURL: string,
    defaultVoice: string = DEFAULT_VOICE,
    defaultFormat: AudioFormat = DEFAULT_FORMAT
  ) {
    this.baseURL = baseURL;
    this.defaultVoice = defaultVoice;
    this.defaultFormat = defaultFormat;
    this.logger = new Logger(this.constructor.name);

    this.client = new OpenAI({
      baseURL,
      apiKey: "not-needed", // Self-hosted services don't require an API key
    });
  }

  /**
   * Synthesize text to audio using the OpenAI-compatible TTS endpoint.
   *
   * Calls `client.audio.speech.create()` with the provided text and options,
   * then converts the response to a Buffer.
   *
   * @param text - Text to convert to speech
   * @param options - Optional synthesis parameters (voice, format, speed)
   * @returns Synthesis result with audio buffer and metadata
   * @throws {Error} If synthesis fails
   */
  async synthesize(text: string, options?: SynthesizeOptions): Promise<SynthesisResult> {
    const voice = options?.voice ?? this.defaultVoice;
    const format = options?.format ?? this.defaultFormat;
    const speed = options?.speed ?? DEFAULT_SPEED;

    try {
      const response = await this.client.audio.speech.create({
        model: DEFAULT_MODEL,
        input: text,
        voice,
        response_format: format,
        speed,
      });

      const arrayBuffer = await response.arrayBuffer();
      const audio = Buffer.from(arrayBuffer);

      return {
        audio,
        format,
        voice,
        tier: this.tier,
      };
    } catch (error: unknown) {
      const message = error instanceof Error ? error.message : String(error);
      this.logger.error(`TTS synthesis failed: ${message}`);
      throw new Error(`TTS synthesis failed for ${this.name}: ${message}`);
    }
  }

  /**
   * List available voices for this provider.
   *
   * Default implementation returns the configured default voice.
   * Subclasses should override this to provide a full voice list
   * from their specific TTS engine.
   *
   * @returns Array of voice information objects
   */
  listVoices(): Promise<VoiceInfo[]> {
    return Promise.resolve([
      {
        id: this.defaultVoice,
        name: this.defaultVoice,
        tier: this.tier,
        isDefault: true,
      },
    ]);
  }

  /**
   * Check if the TTS server is reachable and healthy.
   *
   * Performs a simple HTTP request to the base URL's models endpoint
   * to verify the server is running and responding.
   *
   * @returns true if the server is reachable, false otherwise
   */
  async isHealthy(): Promise<boolean> {
    try {
      // Extract the base URL without the /v1 path for health checking
      const healthUrl = this.baseURL.replace(/\/v1\/?$/, "/v1/models");
      const controller = new AbortController();
      const timeoutId = setTimeout(() => {
        controller.abort();
      }, HEALTH_CHECK_TIMEOUT_MS);

      try {
        const response = await fetch(healthUrl, {
          method: "GET",
          signal: controller.signal,
        });

        return response.ok;
      } finally {
        clearTimeout(timeoutId);
      }
    } catch (error: unknown) {
      const message = error instanceof Error ? error.message : String(error);
      this.logger.warn(`Health check failed for ${this.name}: ${message}`);
      return false;
    }
  }
}
