package com.meowarex.rlmobile.ui.screens.patching

import android.annotation.SuppressLint
import android.app.Application
import android.util.Log
import androidx.annotation.StringRes
import androidx.compose.runtime.*
import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.screenModelScope
import com.meowarex.rlmobile.BuildConfig
import com.meowarex.rlmobile.R
import com.meowarex.rlmobile.manager.*
import com.meowarex.rlmobile.patcher.TidalPatchRunner
import com.meowarex.rlmobile.patcher.StepRunner
import com.meowarex.rlmobile.patcher.steps.StepGroup
import com.meowarex.rlmobile.patcher.steps.base.*
import com.meowarex.rlmobile.patcher.steps.install.InstallStep
import com.meowarex.rlmobile.patcher.util.InsufficientStorageException
import com.meowarex.rlmobile.ui.screens.patchopts.PatchOptions
import com.meowarex.rlmobile.ui.util.toUnsafeImmutable
import com.meowarex.rlmobile.util.*
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.ImmutableMap
import kotlinx.coroutines.*
import java.util.UUID
import kotlin.time.*
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds

class PatchingScreenModel(
    private val options: PatchOptions,
    private val paths: PathManager,
    private val prefs: PreferencesManager,
    private val application: Application,
    private val installLogs: InstallLogManager,
) : StateScreenModel<PatchingScreenState>(PatchingScreenState.Working) {
    private var installId: String? = null
    private var startTime: Instant? = null
    private var runnerJob: Job? = null
    private var stepRunner: StepRunner? = null

    val devMode get() = prefs.devMode

    var steps by mutableStateOf<ImmutableMap<StepGroup, ImmutableList<Step>>?>(null)
        private set

    @get:StringRes
    var funFact by mutableIntStateOf(0)
        private set

    init {
        install()

        // Rotate fun facts every so often
        screenModelScope.launch {
            while (true) {
                funFact = FUN_FACTS.random()
                delay(8.seconds)
            }
        }
    }

    fun launchApp() {
        if (state.value !is PatchingScreenState.Success)
            return

        val launchIntent = application.packageManager
            .getLaunchIntentForPackage(options.packageName)

        if (launchIntent != null) {
            application.startActivity(launchIntent)
        } else {
            application.showToast(R.string.launch_app_fail)
        }
    }

    fun clearCache() {
        paths.clearCache()
        application.showToast(R.string.action_cleared_cache)
    }

    fun getCurrentInstallId(): String? = installId

    fun cancelInstall() = screenModelScope.launchIO {
        runnerJob?.cancel("Manual cancellation")

        // Delete any in-progress downloads to be safe
        stepRunner?.also { container ->
            val incompleteDownloadStep = container.steps
                .filterIsInstance<DownloadStep<*>>()
                .lastOrNull { it.state == StepState.Running }

            incompleteDownloadStep?.getStoredFile(container)?.delete()
        }

        paths.patchingWorkingDir.deleteRecursively()
    }

    fun install() = screenModelScope.launchBlock {
        runnerJob?.cancel("Manual cancellation")
        mainThread { steps = null }

        @SuppressLint("MemberExtensionConflict")
        installId = UUID.randomUUID().toString()
        startTime = Clock.System.now()
        mutableState.value = PatchingScreenState.Working

        runnerJob = screenModelScope.launch(Dispatchers.Default) {
            Log.i(BuildConfig.TAG, "Starting installation with environment:\n" + installLogs.getEnvironmentInfo())

            try {
                startPatchRunner()
            } catch (_: CancellationException) {
                Log.w(BuildConfig.TAG, "Installation was cancelled before completion")
                mutableState.value = PatchingScreenState.CloseScreen
            } catch (error: Throwable) {
                Log.e(BuildConfig.TAG, "Failed to orchestrate patch runner", error)
                mutableState.value = PatchingScreenState.Failed(installId = installId!!)
                installLogs.storeInstallData(
                    id = installId!!,
                    installDate = startTime!!,
                    installDuration = Duration.ZERO,
                    options = options,
                    log = "- Failed to initialize patch runner",
                    error = error,
                )
            }
        }
    }

    private suspend fun startPatchRunner() {
        val runner = TidalPatchRunner(options)
            .also { stepRunner = it }

        val newSteps = runner.steps.groupBy { it.group }
            .mapValues { it.value.toUnsafeImmutable() }
            .toUnsafeImmutable()
        mainThread { steps = newSteps }

        // Tiny delay so the first step's progress is visible on fast connections
        delay(400)

        // Execute all the steps and catch any errors
        val error = when (val error = runner.executeAll()) {
            null -> {
                // Skipped install step means user aborted, close screen
                if (runner.getStep<InstallStep>().state == StepState.Skipped) {
                    mutableState.value = PatchingScreenState.CloseScreen

                    Error("Installation was aborted or cancelled")
                        .apply { stackTrace = emptyArray() }
                }
                // At this point, the installation has successfully completed
                else {
                    mutableState.value = PatchingScreenState.Success

                    null
                }
            }

            else -> {
                Log.e(BuildConfig.TAG, "Failed to perform installation process", error)
                mutableState.value = PatchingScreenState.Failed(installId = installId!!)

                if (error is InsufficientStorageException) {
                    mainThread { application.showToast(R.string.installer_insufficient_storage) }
                }

                error
            }
        }

        installLogs.storeInstallData(
            id = installId!!,
            installDate = startTime!!,
            installDuration = runner.steps.sumOf { it.getDuration() }.milliseconds,
            options = options,
            log = runner.getLog(),
            error = error,
        )
    }

    companion object {
        /**
         * Random fun facts to show on the installation screen.
         */
        private val FUN_FACTS = arrayOf(
            R.string.fun_fact_1,
            R.string.fun_fact_2,
            R.string.fun_fact_3,
            R.string.fun_fact_4,
            R.string.fun_fact_5,
            R.string.fun_fact_6,
            R.string.fun_fact_7,
            R.string.fun_fact_8,
            R.string.fun_fact_9,
            R.string.fun_fact_10,
            R.string.fun_fact_11,
            R.string.fun_fact_12,
            R.string.fun_fact_13,
        )
    }
}
