/**
 * TTSPlayer synthesizes text via kokoro-fastapi, manages a playback
 * queue, and plays audio. Extends EventTarget for playback state
 * notifications.
 *
 * Events dispatched:
 *   'playbackstart'  { detail: { messageId } }
 *   'playbackend'    { detail: { messageId } }
 *   'stopped'        (no detail)
 */
export class TTSPlayer extends EventTarget {
  /**
   * @param {Object} options
   * @param {string} options.url - kokoro-fastapi base URL
   * @param {string} options.defaultVoice
   */
  constructor({ url, defaultVoice }) {
    super();
    this.url = url;
    this.defaultVoice = defaultVoice;
    this.playQueue = [];
    this._playing = false;
    this._currentMessageId = null;
    this.currentAudio = null;
    this.currentAudioURL = null;
    this._muted = false;
  }

  /**
   * enqueue adds a message to the TTS playback queue and starts
   * playback if not already playing.
   * @param {string} text
   * @param {string} voice
   * @param {string} messageId
   */
  enqueue(text, voice, messageId) {
    if (!text || !this.url) return;
    this.playQueue.push({ text, voice, messageId });
    if (!this._playing) {
      this._processQueue();
    }
  }

  /**
   * stop stops current audio playback, clears the queue, and resets.
   */
  stop() {
    this._stopCurrentAudio();
    this.playQueue = [];
    this._playing = false;

    if (this.currentMessageId) {
      this.dispatchEvent(
        new CustomEvent('playbackend', {
          detail: { messageId: this.currentMessageId },
        }),
      );
    }
    this._currentMessageId = null;
    this.dispatchEvent(new CustomEvent('stopped'));
  }

  /**
   * setMuted updates the mute state for current and future playback.
   * @param {boolean} muted
   */
  setMuted(muted) {
    this._muted = muted;
    if (this.currentAudio) {
      this.currentAudio.muted = muted;
    }
  }

  /** @returns {boolean} */
  get isPlaying() {
    return this._playing;
  }

  /** @returns {string|null} */
  get currentMessageId() {
    return this._currentMessageId;
  }

  // Private implementation

  /**
   * _processQueue pops the next item and synthesizes/plays it.
   * @private
   */
  async _processQueue() {
    if (this.playQueue.length === 0) {
      this._playing = false;
      this._currentMessageId = null;
      return;
    }

    this._playing = true;
    const item = this.playQueue.shift();
    this._currentMessageId = item.messageId;

    this.dispatchEvent(
      new CustomEvent('playbackstart', {
        detail: { messageId: item.messageId },
      }),
    );

    await this._synthesizeAndPlay(item.text, item.voice);

    this.dispatchEvent(
      new CustomEvent('playbackend', {
        detail: { messageId: item.messageId },
      }),
    );

    // Move to next item in queue.
    this._processQueue();
  }

  /**
   * _synthesizeAndPlay calls kokoro-fastapi to synthesize text and
   * plays the resulting audio.
   * @param {string} text
   * @param {string} voice
   * @private
   */
  async _synthesizeAndPlay(text, voice) {
    try {
      this._stopCurrentAudio();

      const res = await fetch(`${this.url}/v1/audio/speech`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          model: 'tts-1',
          input: text,
          voice: voice || this.defaultVoice,
          response_format: 'wav',
        }),
      });

      if (!res.ok) {
        throw new Error(`TTS error ${res.status}`);
      }

      const audioBlob = await res.blob();
      const audioURL = URL.createObjectURL(audioBlob);

      this.currentAudioURL = audioURL;
      this.currentAudio = new Audio(audioURL);
      this.currentAudio.muted = this._muted;

      await new Promise((resolve, reject) => {
        this.currentAudio.addEventListener(
          'ended',
          () => {
            this._cleanupAudio();
            resolve();
          },
          { once: true },
        );
        this.currentAudio.addEventListener(
          'error',
          (e) => {
            this._cleanupAudio();
            reject(e);
          },
          { once: true },
        );
        this.currentAudio.play().catch(reject);
      });
    } catch (e) {
      console.error('TTS synthesis failed:', e);
      this._cleanupAudio();
    }
  }

  /**
   * _stopCurrentAudio stops and cleans up any playing audio.
   * @private
   */
  _stopCurrentAudio() {
    if (this.currentAudio) {
      this.currentAudio.pause();
      this.currentAudio.currentTime = 0;
    }
    this._cleanupAudio();
  }

  /**
   * _cleanupAudio revokes the object URL and clears audio references.
   * @private
   */
  _cleanupAudio() {
    if (this.currentAudioURL) {
      URL.revokeObjectURL(this.currentAudioURL);
      this.currentAudioURL = null;
    }
    this.currentAudio = null;
  }
}
