package net.sergeych.toread

import android.Manifest
import android.annotation.SuppressLint
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.os.IBinder
import android.speech.tts.TextToSpeech
import android.speech.tts.UtteranceProgressListener
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
import java.util.Locale
import java.util.concurrent.atomic.AtomicLong
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock

actual object ReadAloudPlatform {
    actual val isSupported: Boolean = true
    actual val state: StateFlow<ReadAloudState> = AndroidReadAloudEngine.state
    actual val settingsState: StateFlow<ReadAloudSettingsState> = AndroidReadAloudEngine.settingsState

    actual fun prepare(fileId: String, bookTitle: String, sentences: List<ReadAloudSentence>, startIndex: Int) {
        val context = androidAppContext() ?: return
        AndroidReadAloudEngine.prepare(context, fileId, bookTitle, sentences, startIndex)
        ReadAloudService.start(context)
    }

    actual fun play() {
        val context = androidAppContext() ?: return
        ReadAloudService.start(context)
        AndroidReadAloudEngine.play(context)
    }

    actual fun stop() {
        AndroidReadAloudEngine.stop()
    }

    actual fun skip(delta: Int) {
        AndroidReadAloudEngine.skip(delta)
    }

    actual fun refreshSettings() {
        val context = androidAppContext() ?: return
        AndroidReadAloudEngine.refreshSettings(context)
    }

    actual fun selectEngine(engineId: String?) {
        val context = androidAppContext() ?: return
        AndroidReadAloudEngine.selectEngine(context, engineId)
    }

    actual fun selectVoice(voiceId: String?) {
        val context = androidAppContext() ?: return
        AndroidReadAloudEngine.selectVoice(context, voiceId)
    }
}

private object AndroidReadAloudEngine {
    private const val UtterancePrefix = "read-aloud-"
    private const val BeforePausePrefix = "read-aloud-before-"
    private const val SpeakPrefix = "read-aloud-speak-"
    private const val AfterPausePrefix = "read-aloud-after-"
    private const val SettingsPrefs = "read_aloud_tts"
    private const val SelectedEngineKey = "selected_engine"
    private const val EngineChoiceMadeKey = "engine_choice_made"
    private const val SelectedVoiceKey = "selected_voice"
    private const val TextReplacementsKey = "text_replacements"
    private const val EmptyTextReplacementsJson = "[]"
    private val RussianLocale = Locale.forLanguageTag("ru-RU")

    private val mutableState = MutableStateFlow(ReadAloudState())
    val state: StateFlow<ReadAloudState> = mutableState
    private val mutableSettingsState = MutableStateFlow(ReadAloudSettingsState())
    val settingsState: StateFlow<ReadAloudSettingsState> = mutableSettingsState
    private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
    private val positionSaveMutex = Mutex()
    private val positionSaveVersion = AtomicLong()

    private var tts: TextToSpeech? = null
    private var ttsReady = false
    private var ttsEngineId: String? = null
    private var engineProbe: TextToSpeech? = null
    private var voiceProbe: TextToSpeech? = null
    private var shouldSpeakWhenReady = false
    private var fileId: String? = null
    private var bookTitle: String = ""
    private var sentences: List<ReadAloudSentence> = emptyList()
    private var currentIndex: Int = 0

    fun prepare(context: Context, fileId: String, title: String, queue: List<ReadAloudSentence>, startIndex: Int) {
        this.fileId = fileId
        bookTitle = title
        sentences = queue
        currentIndex = startIndex.coerceIn(queue.indices.takeIf { queue.isNotEmpty() } ?: 0..0)
        mutableState.value = ReadAloudState(
            active = queue.isNotEmpty(),
            playing = false,
            sentenceIndex = queue.getOrNull(currentIndex)?.index,
        )
        ensureTts(context.applicationContext)
    }

    fun play(context: Context) {
        if (sentences.isEmpty()) return
        ensureTts(context.applicationContext)
        if (!ttsReady) {
            shouldSpeakWhenReady = true
            mutableState.value = mutableState.value.copy(
                active = true,
                playing = true,
                sentenceIndex = sentences.getOrNull(currentIndex)?.index,
            )
            return
        }
        speakCurrent()
    }

    fun stop() {
        shouldSpeakWhenReady = false
        tts?.stop()
        mutableState.value = ReadAloudState()
    }

    fun skip(delta: Int) {
        if (sentences.isEmpty()) return
        val wasPlaying = mutableState.value.playing
        currentIndex = (currentIndex + delta).coerceIn(sentences.indices)
        val sentence = sentences.getOrNull(currentIndex)
        mutableState.value = mutableState.value.copy(
            active = true,
            playing = wasPlaying,
            sentenceIndex = sentence?.index ?: currentIndex,
        )
        sentence?.let(::saveSentencePosition)
        if (wasPlaying && ttsReady) {
            speakCurrent()
        }
    }

    fun refreshSettings(context: Context) {
        val appContext = context.applicationContext
        val textReplacements = userTextReplacements(appContext)
        mutableSettingsState.value = mutableSettingsState.value.copy(
            textReplacements = textReplacements,
            loading = true,
            message = null,
        )
        engineProbe?.shutdown()
        var probe: TextToSpeech? = null
        probe = TextToSpeech(appContext) { status ->
            if (status != TextToSpeech.SUCCESS) {
                mutableSettingsState.value = mutableSettingsState.value.copy(
                    loading = false,
                    message = strings.couldNotLoadAndroidTtsEngines,
                )
                probe?.shutdown()
                if (engineProbe === probe) engineProbe = null
                return@TextToSpeech
            }

            val engines = probe?.engines.orEmpty()
                .map { ReadAloudEngineOption(id = it.name, label = it.label ?: it.name) }
                .sortedBy { it.label.lowercase() }
            var selectedEngine = selectedEngineId(appContext).takeIf { saved ->
                saved != null && engines.any { it.id == saved }
            }
            if (selectedEngine == null && !appContext.readAloudPrefs().getBoolean(EngineChoiceMadeKey, false)) {
                selectedEngine = engines.firstOrNull { it.isRhVoice() }?.id
                if (selectedEngine != null) {
                    appContext.readAloudPrefs().edit().putString(SelectedEngineKey, selectedEngine).apply()
                }
            }
            mutableSettingsState.value = mutableSettingsState.value.copy(
                engines = engines,
                selectedEngineId = selectedEngine,
                textReplacements = textReplacements,
            )
            probe?.shutdown()
            if (engineProbe === probe) engineProbe = null
            refreshVoices(appContext, selectedEngine)
        }
        engineProbe = probe
    }

    fun selectEngine(context: Context, engineId: String?) {
        val appContext = context.applicationContext
        appContext.readAloudPrefs()
            .edit()
            .putBoolean(EngineChoiceMadeKey, true)
            .putStringOrRemove(SelectedEngineKey, engineId)
            .remove(SelectedVoiceKey)
            .apply()
        resetTts()
        mutableSettingsState.value = mutableSettingsState.value.copy(
            selectedEngineId = engineId,
            selectedVoiceId = null,
            voices = emptyList(),
            loading = true,
            message = null,
        )
        refreshVoices(appContext, engineId)
    }

    fun selectVoice(context: Context, voiceId: String?) {
        val appContext = context.applicationContext
        appContext.readAloudPrefs()
            .edit()
            .putStringOrRemove(SelectedVoiceKey, voiceId)
            .apply()
        mutableSettingsState.value = mutableSettingsState.value.copy(selectedVoiceId = voiceId)
        applyTtsConfiguration(appContext)
        if (mutableState.value.playing && ttsReady) {
            speakCurrent()
        }
    }

    private fun ensureTts(context: Context) {
        val appContext = context.applicationContext
        val selectedEngine = selectedEngineId(appContext)
        if (tts != null && ttsEngineId == selectedEngine) return
        resetTts()
        ttsEngineId = selectedEngine
        tts = if (selectedEngine == null) {
            TextToSpeech(appContext) { status ->
                handleTtsInit(appContext, status)
            }
        } else {
            TextToSpeech(
                appContext,
                { status -> handleTtsInit(appContext, status) },
                selectedEngine,
            )
        }
    }

    private fun handleTtsInit(context: Context, status: Int) {
        ttsReady = status == TextToSpeech.SUCCESS
        if (ttsReady) {
            applyTtsConfiguration(context)
            tts?.setOnUtteranceProgressListener(object : UtteranceProgressListener() {
                override fun onStart(utteranceId: String?) = Unit

                override fun onDone(utteranceId: String?) {
                    handleUtteranceDone(utteranceId)
                }

                @Deprecated("Deprecated in Java")
                override fun onError(utteranceId: String?) {
                    stop()
                }

                override fun onError(utteranceId: String?, errorCode: Int) {
                    stop()
                }
            })
            if (shouldSpeakWhenReady) {
                shouldSpeakWhenReady = false
                speakCurrent()
            }
        } else {
            stop()
        }
    }

    private fun speakCurrent() {
        val sentence = sentences.getOrNull(currentIndex) ?: run {
            stop()
            return
        }
        mutableState.value = ReadAloudState(active = true, playing = true, sentenceIndex = sentence.index)
        saveSentencePosition(sentence)
        val params = Bundle().apply {
            putString(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, "$SpeakPrefix$currentIndex")
        }
        val queueMode = if (sentence.pauseBeforeMillis > 0) {
            tts?.playSilentUtterance(sentence.pauseBeforeMillis, TextToSpeech.QUEUE_FLUSH, "$BeforePausePrefix$currentIndex")
            TextToSpeech.QUEUE_ADD
        } else {
            TextToSpeech.QUEUE_FLUSH
        }
        tts?.speak(sentence.spokenText, queueMode, params, "$SpeakPrefix$currentIndex")
        if (sentence.pauseAfterMillis > 0) {
            tts?.playSilentUtterance(sentence.pauseAfterMillis, TextToSpeech.QUEUE_ADD, "$AfterPausePrefix$currentIndex")
        }
    }

    private fun handleUtteranceDone(utteranceId: String?) {
        if (!mutableState.value.playing) return
        val speakIndex = utteranceId?.removePrefix(SpeakPrefix)?.takeIf { it != utteranceId }?.toIntOrNull()
        val afterIndex = utteranceId?.removePrefix(AfterPausePrefix)?.takeIf { it != utteranceId }?.toIntOrNull()
        val finishedIndex = afterIndex ?: speakIndex ?: return
        if (finishedIndex != currentIndex) return
        val currentSentence = sentences.getOrNull(currentIndex) ?: run {
            stop()
            return
        }
        if (speakIndex != null && currentSentence.pauseAfterMillis > 0) {
            sentences.getOrNull(currentIndex + 1)?.let { nextSentence ->
                mutableState.value = mutableState.value.copy(sentenceIndex = nextSentence.index)
                saveSentencePosition(nextSentence)
            }
            return
        }
        if (currentIndex < sentences.lastIndex) {
            currentIndex += 1
            speakCurrent()
        } else {
            stop()
        }
    }

    fun notificationTitle(): String = bookTitle.ifBlank { strings.readAloud }

    private fun saveSentencePosition(sentence: ReadAloudSentence) {
        val activeFileId = fileId ?: return
        val saveVersion = positionSaveVersion.incrementAndGet()
        scope.launch {
            positionSaveMutex.withLock {
                if (saveVersion != positionSaveVersion.get()) return@withLock
                runCatching {
                    saveLibraryReadingPosition(activeFileId, ReadingPosition(sentence.itemIndex, 0, sentence.index))
                }
            }
        }
    }

    private fun refreshVoices(context: Context, engineId: String?) {
        voiceProbe?.shutdown()
        var probe: TextToSpeech? = null
        probe = if (engineId == null) {
            TextToSpeech(context) { status ->
                handleVoiceProbeInit(context, probe, status)
            }
        } else {
            TextToSpeech(
                context,
                { status -> handleVoiceProbeInit(context, probe, status) },
                engineId,
            )
        }
        voiceProbe = probe
    }

    private fun handleVoiceProbeInit(context: Context, probe: TextToSpeech?, status: Int) {
        if (status != TextToSpeech.SUCCESS) {
            mutableSettingsState.value = mutableSettingsState.value.copy(
                loading = false,
                message = strings.couldNotLoadSelectedTtsVoices,
            )
            probe?.shutdown()
            if (voiceProbe === probe) voiceProbe = null
            return
        }

        val voices = probe?.voices.orEmpty()
            .mapNotNull { voice ->
                val locale = voice.locale ?: return@mapNotNull null
                ReadAloudVoiceOption(
                    id = voice.name,
                    label = voice.name,
                    localeTag = locale.toLanguageTag(),
                    networkRequired = voice.isNetworkConnectionRequired,
                )
            }
            .filter { !it.networkRequired }
            .sortedWith(
                compareByDescending<ReadAloudVoiceOption> { it.localeTag.startsWith("ru", ignoreCase = true) }
                    .thenBy { it.localeTag }
                    .thenBy { it.label.lowercase() },
            )
        val savedVoice = selectedVoiceId(context).takeIf { saved ->
            saved != null && voices.any { it.id == saved }
        }
        mutableSettingsState.value = mutableSettingsState.value.copy(
            voices = voices,
            selectedVoiceId = savedVoice,
            loading = false,
            message = if (voices.isEmpty()) strings.noOfflineVoices else null,
        )
        if (savedVoice == null && selectedVoiceId(context) != null) {
            context.readAloudPrefs().edit().remove(SelectedVoiceKey).apply()
        }
        probe?.shutdown()
        if (voiceProbe === probe) voiceProbe = null
    }

    private fun applyTtsConfiguration(context: Context) {
        val selectedVoice = selectedVoiceId(context)
        val voice = selectedVoice?.let { voiceName ->
            tts?.voices?.firstOrNull { it.name == voiceName && !it.isNetworkConnectionRequired }
        }
        if (voice != null) {
            tts?.voice = voice
        } else {
            tts?.setLanguage(RussianLocale)
        }
    }

    private fun resetTts() {
        shouldSpeakWhenReady = false
        ttsReady = false
        ttsEngineId = null
        tts?.stop()
        tts?.shutdown()
        tts = null
    }

    private fun selectedEngineId(context: Context): String? =
        context.readAloudPrefs().getString(SelectedEngineKey, null)

    private fun selectedVoiceId(context: Context): String? =
        context.readAloudPrefs().getString(SelectedVoiceKey, null)

    private fun userTextReplacements(context: Context): List<ReadAloudTextReplacement> {
        val prefs = context.readAloudPrefs()
        if (!prefs.contains(TextReplacementsKey)) {
            prefs.edit().putString(TextReplacementsKey, EmptyTextReplacementsJson).apply()
        }
        return emptyList()
    }

    private fun Context.readAloudPrefs() =
        getSharedPreferences(SettingsPrefs, Context.MODE_PRIVATE)

    private fun ReadAloudEngineOption.isRhVoice(): Boolean =
        id.contains("rhvoice", ignoreCase = true) || label.contains("RHVoice", ignoreCase = true)
}

private fun android.content.SharedPreferences.Editor.putStringOrRemove(
    key: String,
    value: String?,
): android.content.SharedPreferences.Editor =
    if (value == null) remove(key) else putString(key, value)

class ReadAloudService : Service() {
    private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)

    override fun onCreate() {
        super.onCreate()
        createNotificationChannel()
        scope.launch {
            AndroidReadAloudEngine.state.collect { state ->
                if (state.active) {
                    updateNotification(state)
                } else {
                    stopForegroundCompat()
                    stopSelf()
                }
            }
        }
    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        startForeground(NotificationId, buildNotification(AndroidReadAloudEngine.state.value))
        when (intent?.action) {
            ActionPlay -> AndroidReadAloudEngine.play(applicationContext)
            ActionStop -> AndroidReadAloudEngine.stop()
            ActionBack -> AndroidReadAloudEngine.skip(-1)
            ActionForward -> AndroidReadAloudEngine.skip(1)
        }
        return START_STICKY
    }

    override fun onBind(intent: Intent?): IBinder? = null

    override fun onDestroy() {
        scope.cancel()
        super.onDestroy()
    }

    private fun buildNotification(state: ReadAloudState): Notification {
        val openIntent = packageManager.getLaunchIntentForPackage(packageName)
        val contentIntent = PendingIntent.getActivity(
            this,
            0,
            openIntent,
            PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
        )
        val playStopAction = if (state.playing) ActionStop else ActionPlay
        val playStopTitle = if (state.playing) strings.stop else strings.play
        val playStopIcon = if (state.playing) android.R.drawable.ic_media_pause else android.R.drawable.ic_media_play

        return NotificationCompat.Builder(this, ChannelId)
            .setSmallIcon(R.drawable.ic_launcher_background)
            .setContentTitle(AndroidReadAloudEngine.notificationTitle())
            .setContentText(strings.readingInBackground)
            .setContentIntent(contentIntent)
            .setOngoing(state.playing)
            .setOnlyAlertOnce(true)
            .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
            .addAction(android.R.drawable.ic_media_previous, strings.previous, serviceIntent(ActionBack))
            .addAction(playStopIcon, playStopTitle, serviceIntent(playStopAction))
            .addAction(android.R.drawable.ic_media_next, strings.next, serviceIntent(ActionForward))
            .build()
    }

    private fun serviceIntent(action: String): PendingIntent =
        PendingIntent.getService(
            this,
            action.hashCode(),
            Intent(this, ReadAloudService::class.java).setAction(action),
            PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
        )

    @SuppressLint("MissingPermission")
    private fun updateNotification(state: ReadAloudState) {
        if (!hasNotificationPermission()) return
        runCatching {
            NotificationManagerCompat.from(this)
                .notify(NotificationId, buildNotification(state))
        }
    }

    private fun createNotificationChannel() {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
        val channel = NotificationChannel(
            ChannelId,
            strings.readAloud,
            NotificationManager.IMPORTANCE_LOW,
        )
        getSystemService(NotificationManager::class.java).createNotificationChannel(channel)
    }

    private fun hasNotificationPermission(): Boolean =
        Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU ||
            ContextCompat.checkSelfPermission(
                this,
                Manifest.permission.POST_NOTIFICATIONS,
            ) == PackageManager.PERMISSION_GRANTED

    private fun stopForegroundCompat() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            stopForeground(STOP_FOREGROUND_REMOVE)
        } else {
            @Suppress("DEPRECATION")
            stopForeground(true)
        }
    }

    companion object {
        private const val ChannelId = "read_aloud"
        private const val NotificationId = 2401
        private const val ActionPlay = "net.sergeych.toread.READ_ALOUD_PLAY"
        private const val ActionStop = "net.sergeych.toread.READ_ALOUD_STOP"
        private const val ActionBack = "net.sergeych.toread.READ_ALOUD_BACK"
        private const val ActionForward = "net.sergeych.toread.READ_ALOUD_FORWARD"

        fun start(context: Context) {
            val intent = Intent(context, ReadAloudService::class.java)
            ContextCompat.startForegroundService(context, intent)
        }
    }
}
