import path from 'node:path'
import events from 'node:events'
import fs from 'node:fs'

import type { ShortLanguageCode } from '@/types'
import type { TTSSynthesizer } from '@/core/tts/types'
import { BRAIN, SOCKET_SERVER } from '@/core'
import { SERVER_CORE_PATH, TTS_PROVIDER, VOICE_CONFIG_PATH } from '@/constants'
import { TTSSynthesizers, TTSProviders } from '@/core/tts/types'
import { LogHelper } from '@/helpers/log-helper'
import { LangHelper } from '@/helpers/lang-helper'

interface Speech {
  text: string
  isFinalAnswer: boolean
}

const PROVIDERS_MAP = {
  [TTSProviders.Local]: TTSSynthesizers.Local,
  [TTSProviders.GoogleCloudTTS]: TTSSynthesizers.GoogleCloudTTS,
  [TTSProviders.WatsonTTS]: TTSSynthesizers.WatsonTTS,
  [TTSProviders.AmazonPolly]: TTSSynthesizers.AmazonPolly,
  [TTSProviders.Flite]: TTSSynthesizers.Flite
}

export default class TTS {
  private static instance: TTS

  private synthesizer: TTSSynthesizer = undefined
  public speeches: Speech[] = []

  public lang: ShortLanguageCode = 'en'
  public em = new events.EventEmitter()

  constructor() {
    if (!TTS.instance) {
      LogHelper.title('TTS')
      LogHelper.success('New instance')

      TTS.instance = this
    }
  }

  /**
   * Initialize the TTS provider
   */
  public async init(newLang: ShortLanguageCode): Promise<boolean> {
    LogHelper.title('TTS')
    LogHelper.info('Initializing TTS...')

    this.lang = newLang || this.lang

    if (!Object.values(TTSProviders).includes(TTS_PROVIDER as TTSProviders)) {
      LogHelper.error(
        `The TTS provider "${TTS_PROVIDER}" does not exist or is not yet supported`
      )

      return false
    }

    if (
      TTS_PROVIDER === TTSProviders.GoogleCloudTTS &&
      typeof process.env['GOOGLE_APPLICATION_CREDENTIALS'] === 'undefined'
    ) {
      process.env['GOOGLE_APPLICATION_CREDENTIALS'] = path.join(
        VOICE_CONFIG_PATH,
        'google-cloud.json'
      )
    } else if (
      typeof process.env['GOOGLE_APPLICATION_CREDENTIALS'] !== 'undefined' &&
      process.env['GOOGLE_APPLICATION_CREDENTIALS'].indexOf(
        'google-cloud.json'
      ) === -1
    ) {
      LogHelper.warning(
        `The "GOOGLE_APPLICATION_CREDENTIALS" env variable is already settled with the following value: "${process.env['GOOGLE_APPLICATION_CREDENTIALS']}"`
      )
    }

    try {
      // Dynamically attribute the synthesizer
      const { default: synthesizer } = await import(
        path.join(
          SERVER_CORE_PATH,
          'tts',
          'synthesizers',
          PROVIDERS_MAP[TTS_PROVIDER as TTSProviders]
        )
      )
      this.synthesizer = new synthesizer(
        LangHelper.getLongCode(this.lang)
      ) as TTSSynthesizer

      this.onSaved()

      LogHelper.title('TTS')
      LogHelper.success('TTS initialized')

      return true
    } catch (e) {
      LogHelper.error(`The TTS provider failed to initialize: ${e}`)
      process.exit(1)
    }
  }

  /**
   * Forward buffer audio file and duration to the client
   * and delete audio file once it has been forwarded
   */
  private async forward(speech: Speech): Promise<void> {
    if (this.synthesizer) {
      BRAIN.setIsTalkingWithVoice(true)

      const result = await this.synthesizer.synthesize(speech.text)

      // Support custom TTS providers such as the local synthesizer
      if (result?.audioFilePath === '') {
        return
      }

      if (!result) {
        LogHelper.error(
          'The TTS synthesizer failed to synthesize the speech as the result is null'
        )
      } else {
        const { audioFilePath, duration } = result
        const bitmap = await fs.promises.readFile(audioFilePath)

        SOCKET_SERVER.socket?.emit(
          'audio-forwarded',
          {
            buffer: Buffer.from(bitmap),
            is_final_answer: speech.isFinalAnswer,
            duration
          },
          (confirmation: string) => {
            if (confirmation === 'audio-received' && audioFilePath !== '') {
              fs.unlinkSync(audioFilePath)
            }
          }
        )
      }
    } else {
      LogHelper.error('The TTS synthesizer is not initialized yet')
    }
  }

  /**
   * When the synthesizer saved a new audio file
   * then shift the queue according to the audio file duration
   */
  private onSaved(): void {
    this.em.on('saved', (duration) => {
      setTimeout(async () => {
        this.speeches.shift()

        BRAIN.setIsTalkingWithVoice(false)

        if (this.speeches[0]) {
          await this.forward(this.speeches[0])
        }
      }, duration)
    })
  }

  /**
   * Add speeches to the queue
   */
  public async add(
    text: Speech['text'],
    isFinalAnswer: Speech['isFinalAnswer']
  ): Promise<Speech[]> {
    /**
     * Flite fix. When the string is only one word,
     * Flite cannot save to a file. So we add a space at the end of the string
     */
    if (TTS_PROVIDER === TTSProviders.Flite && text.indexOf(' ') === -1) {
      text += ' '
    }

    const speech = { text, isFinalAnswer }

    if (this.speeches.length > 0) {
      this.speeches.push(speech)
    } else {
      this.speeches.push(speech)
      await this.forward(speech)
    }

    return this.speeches
  }
}
