// eu/gaudian/translator/utils/TextToSpeechHelper.kt
@file:Suppress("HardCodedStringLiteral")

package eu.gaudian.translator.utils

import android.content.Context
import android.speech.tts.TextToSpeech
import eu.gaudian.translator.R
import eu.gaudian.translator.model.Language
import eu.gaudian.translator.model.repository.SettingsRepository
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import java.util.Locale
import kotlin.coroutines.resume


object TextToSpeechHelper {

    private var textToSpeech: TextToSpeech? = null
    private var languageMap: Map<String, Locale>? = null

    private val playableCache = mutableMapOf<Language, Boolean>()
    private val mutex = Mutex()

    /**
     * Extended speakOut allowing optional voiceName and speaking speed percentage (50..200).
     */
    suspend fun speakOut(context: Context, text: String, language: Language, voiceName: String?) {
        val tts = getInitializedTtsInstance(context) ?: return

        // Ensure a clean state so engine applies new params (voice/rate) reliably on all OEMs
        try { tts.stop() } catch (_: Exception) {}

        val locales = getLanguageMap(context)
        val locale = locales[language.code.lowercase(Locale.getDefault())] ?: Locale.US

        val result = tts.setLanguage(locale)
        if (result == TextToSpeech.LANG_MISSING_DATA || result == TextToSpeech.LANG_NOT_SUPPORTED) {
            Log.e("Language ${language.code} not supported.")
            return
        }

        // Apply voice by name if provided and available
        if (!voiceName.isNullOrBlank()) {
            val v = tts.voices?.firstOrNull { it.name == voiceName }
            if (v != null) {
                try { tts.voice = v } catch (e: Exception) { Log.w("Failed to set TTS voice: ${e.message}") }
            }
        } else {
            // try to select a voice that matches the locale language
            val v = tts.voices?.firstOrNull { it.locale?.language == locale.language }
            if (v != null) {
                try { tts.voice = v } catch (_: Exception) {}
            }
        }

        // Resolve effective speed: prefer provided value; otherwise, use saved setting
        val effectiveSpeedPercent: Int = try {
             SettingsRepository(context).speakingSpeed.flow.first()
        } catch (_: Exception) {
            Log.d("Failed to get speaking speed setting, using default")
             100
        }
        Log.d("Effective speed: $effectiveSpeedPercent")
        val clamped = effectiveSpeedPercent.coerceIn(50, 200)
        val rate = clamped / 100f
        try { tts.setSpeechRate(rate) } catch (e: Exception) { Log.w("Failed to set speech rate: ${e.message}") }
        Log.d("TTS speakOut: lang=${language.code}-${language.region} voice=${voiceName ?: "<default>"} rate=$rate (${clamped}%)")

        // Keep pitch at default (1.0f) to make rate differences clearer
        try { tts.setPitch(1.0f) } catch (_: Exception) {}

        tts.speak(text, TextToSpeech.QUEUE_FLUSH, null, null)
    }

    /**
     * Checks if a given language is supported and playable by the TTS engine.
     * Results are cached for performance.
     *
     * @param language The language to check.
     * @return `true` if the language is supported, `false` otherwise.
     */
    suspend fun isPlayable(context: Context, language: Language?): Boolean {
        if (language == null) return false

        mutex.withLock {
            if (playableCache.containsKey(language)) {
                val cachedValue = playableCache[language]!!
                return cachedValue
            }
        }

        Log.d("Checking if language is playable: $language")
        val tts = getInitializedTtsInstance(context) ?: return false

        val locales = getLanguageMap(context)
        val locale = locales[language.code.lowercase(Locale.getDefault())]

        // 3. Perform the slow check ONLY if the result is not in the cache.
        val result = tts.setLanguage(locale)
        val isSupported = !(result == TextToSpeech.LANG_MISSING_DATA || result == TextToSpeech.LANG_NOT_SUPPORTED)

        // 4. Store the new result in the cache.
        mutex.withLock {
            playableCache[language] = isSupported
        }

        return isSupported
    }


    private suspend fun getInitializedTtsInstance(context: Context): TextToSpeech? {
        textToSpeech?.let { return it }

        return mutex.withLock {
            textToSpeech?.let { return@withLock it }

            var ttsInstance: TextToSpeech? = null
            val success = suspendCancellableCoroutine { continuation ->
                ttsInstance = TextToSpeech(context.applicationContext) { status ->
                    if (continuation.isActive) {
                        continuation.resume(status == TextToSpeech.SUCCESS)
                    }
                }
            }

            if (success) {
                textToSpeech = ttsInstance
                ttsInstance
            } else {
                Log.e("TextToSpeech initialization failed.")
                null
            }
        }
    }

    suspend fun getVoicesForLanguage(context: Context, language: Language): List<android.speech.tts.Voice> {
        val tts = getInitializedTtsInstance(context) ?: return emptyList()
        val locale = getLanguageMap(context)[language.code.lowercase(Locale.getDefault())]
        val voices = tts.voices ?: return emptyList()
        if (locale == null) return voices.toList()

        val langCode = locale.language
        val regionCode = language.region.uppercase(Locale.getDefault())

        // Be lenient: always filter by language first, then prefer exact region match
        val languageMatches = voices.filter { it.locale?.language == langCode }
        if (languageMatches.isNotEmpty()) {
            // Sort so that exact region/country matches come first
            return languageMatches.sortedByDescending { it.locale?.country.equals(regionCode, ignoreCase = true) }
        }
        // Fallback: if OEM reports unexpected locales, show everything rather than empty
        return voices.toList()
    }

    private fun getLanguageMap(context: Context): Map<String, Locale> {
        languageMap?.let { return it }
        val codes = context.resources.getStringArray(R.array.language_codes)
        val newMap = codes.associate {
            val parts = it.split(",")
            val languageName = parts[0].lowercase(Locale.getDefault())
            val localeCode = parts[1]
            languageName to Locale(languageName, localeCode)
        }
        languageMap = newMap
        return newMap
    }
}