package com.fabledsword.minstrel.settings.ui

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.fabledsword.minstrel.BuildConfig
import com.fabledsword.minstrel.api.ErrorCopy
import com.fabledsword.minstrel.models.UpdateInfo
import com.fabledsword.minstrel.update.data.ApkInstaller
import com.fabledsword.minstrel.update.data.InstallStage
import com.fabledsword.minstrel.update.data.UpdateRepository
import com.fabledsword.minstrel.update.data.isBusy
import com.fabledsword.minstrel.update.data.isVersionNewer
import com.fabledsword.minstrel.update.data.message
import com.fabledsword.minstrel.update.data.stage
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import java.io.File
import javax.inject.Inject

/**
 * One of three terminal states the Check-for-updates button surfaces.
 * `Idle` is the pre-check state; `Latest` means the installed build
 * matches or exceeds the server's bundled APK; `UpdateAvailable`
 * surfaces an "Install vX.Y.Z" button that downloads the APK and
 * installs it via [ApkInstaller].
 */
sealed interface UpdateCheckResult {
    data object Idle : UpdateCheckResult
    data object Latest : UpdateCheckResult
    data class UpdateAvailable(val info: UpdateInfo) : UpdateCheckResult
    data class Error(val message: String) : UpdateCheckResult
}

data class AboutUiState(
    val installedVersion: String = BuildConfig.VERSION_NAME,
    val isChecking: Boolean = false,
    val installStage: InstallStage = InstallStage.IDLE,
    val installMessage: String? = null,
    val result: UpdateCheckResult = UpdateCheckResult.Idle,
)

/**
 * Backs the About card's update controls. "Check for updates" calls
 * [UpdateRepository.getLatest], compares versus the build's
 * VERSION_NAME via [isVersionNewer], and reports the terminal state.
 * When an update is available, [install] downloads the APK via
 * [ApkInstaller] and installs it — routing the user to the "install
 * unknown apps" settings page first when that permission hasn't been
 * granted.
 */
@HiltViewModel
class AboutCardViewModel @Inject constructor(
    private val repository: UpdateRepository,
    private val installer: ApkInstaller,
) : ViewModel() {

    private val internal = MutableStateFlow(AboutUiState())
    val state: StateFlow<AboutUiState> = internal.asStateFlow()

    fun checkForUpdates() {
        if (internal.value.isChecking) return
        viewModelScope.launch {
            internal.update { it.copy(isChecking = true, installMessage = null) }
            val installed = internal.value.installedVersion
            val result = runCatching { repository.getLatest() }
                .map { latest ->
                    if (isVersionNewer(latest.version, installed)) {
                        UpdateCheckResult.UpdateAvailable(latest)
                    } else {
                        UpdateCheckResult.Latest
                    }
                }
                .getOrElse { e -> UpdateCheckResult.Error(ErrorCopy.fromThrowable(e)) }
            internal.update { it.copy(isChecking = false, result = result) }
        }
    }

    fun install(info: UpdateInfo) {
        if (internal.value.installStage.isBusy()) return
        if (!installer.canInstall()) {
            installer.requestInstallPermission()
            internal.update {
                it.copy(installMessage = "Allow installs for Minstrel, then tap Install again.")
            }
            return
        }
        viewModelScope.launch {
            internal.update {
                it.copy(installStage = InstallStage.DOWNLOADING, installMessage = null)
            }
            val apk = download(info.apkUrl)
            if (apk != null) {
                // The install half now suspends on the platform's verdict, so it
                // gets its own stage — reporting "Downloading…" through it would
                // be a lie once a confirm dialog is on screen.
                internal.update { it.copy(installStage = InstallStage.INSTALLING) }
                val outcome = installer.install(apk)
                internal.update {
                    it.copy(installStage = outcome.stage(), installMessage = outcome.message())
                }
            }
        }
    }

    private suspend fun download(apkUrl: String): File? =
        runCatching { installer.downloadApk(apkUrl) }
            .onFailure { e ->
                internal.update {
                    it.copy(
                        installStage = InstallStage.ERROR,
                        installMessage = "Couldn't download update: ${ErrorCopy.fromThrowable(e)}",
                    )
                }
            }
            .getOrNull()
}
