package theorycrafter

import androidx.compose.runtime.*
import eve.data.SkillType
import eve.esi.models.GetCharactersCharacterIdSkillsOk
import theorycrafter.esi.EveSsoTokens
import theorycrafter.fitting.FittingEngine
import theorycrafter.fitting.SkillSet
import theorycrafter.storage.FittingRepository
import theorycrafter.storage.StoredSkillSet
import theorycrafter.utils.addNotNull

/**
 * The part of [TheorycrafterContext] responsible for management of skill sets.
 */
class SkillSetsContext(


    /**
     * The underlying repository.
     */
    private val repo: FittingRepository,


    /**
     * The fitting engine.
     */
    private val fittingEngine: FittingEngine,


) {


    /**
     * The handles of built-in skill sets.
     */
    val builtInHandles: List<SkillSetHandle>
        get() = BuiltInSkillSets.All


    /**
     * The handles of built-in skill sets, mapped by their id.
     */
    private val builtInHandleById = builtInHandles.associateBy { it.skillSetId }


    /**
     * The handle of the default skill set.
     *
     * Note that this isn't actually the handle of [FittingEngine.defaultSkillSet]. It's merely the skill set that has
     * been designated as default by [setDefaultSkillSet], which applies all the levels to the fitting engine's
     * [FittingEngine.defaultSkillSet].
     */
    private var _defaultSkillSetHandle: SkillSetHandle? by mutableStateOf(null)


    /**
     * The handle of the default skill set. This can be changed with [setDefaultSkillSet].
     */
    val default: SkillSetHandle
        get() = _defaultSkillSetHandle ?: BuiltInSkillSets.Default


    /**
     * The list of user skill set handles, as a Compose state object.
     */
    private val _userHandles = repo.skillSets
        .sortedBy { it.id!! }
        .map { UserSkillSetHandle(it) }
        .toMutableStateList()


    /**
     * Maps user skill set handles by their ids.
     */
    private val userHandleById: MutableMap<Int, SkillSetHandle> =
        _userHandles
            .associateBy { it.skillSetId }
            .toMutableMap()


    /**
     * The list of user skill set handles, as an unmodifiable list, a Compose state object reflecting the actual list of
     * user skill sets in the repository.
     */
    val userHandles: List<SkillSetHandle>
        get() = _userHandles


    /**
     * Rewrites and replaces the [UserSkillSetHandle.storedSkillSet].
     */
    private suspend fun UserSkillSetHandle.replaceStoredFit(replacement: (StoredSkillSet) -> StoredSkillSet) {
        val current = requireStoredSkillSet()
        val updated = replacement(current)
        repo.replaceSkillSet(current, updated)
        storedSkillSet = updated
    }


    /**
     * Adds the given skill set.
     */
    private suspend fun add(skillSet: StoredSkillSet): SkillSetHandle {
        repo.addSkillSet(skillSet)

        return UserSkillSetHandle(skillSet).also {
            _userHandles.add(it)
            userHandleById[it.skillSetId] = it
        }
    }


    /**
     * Adds a new custom skill set.
     */
    suspend fun add(name: String, levelOfSkill: (SkillType) -> Int): SkillSetHandle {
        return add(
            StoredSkillSet(
                name = name,
                levelBySkillId = fittingEngine.eveData.skillTypes.associate {
                    it.itemId to levelOfSkill(it)
                }
            )
        )
    }


    /**
     * Adds a new skill set obtained via ESI from an EVE character.
     */
    suspend fun add(ssoTokens: EveSsoTokens, characterSkillsInfo: GetCharactersCharacterIdSkillsOk): SkillSetHandle {
        val skillLevelById = characterSkillsInfo.skills.associate { it.skillId to it.activeSkillLevel }
        return add(
            StoredSkillSet(
                name = ssoTokens.characterName,
                levelBySkillId = fittingEngine.eveData.skillTypes.associate {
                    it.itemId to (skillLevelById[it.itemId] ?: UNKNOWN_SKILL_LEVEL)
                },
                ssoTokens = ssoTokens,
                lastUpdateTimeUtcMillis = System.currentTimeMillis()
            )
        )
    }


    /**
     * Sets the given skill set's name.
     */
    suspend fun setName(handle: SkillSetHandle, name: String) {
        if (handle.isBuiltInSkillSet)
            throw IllegalArgumentException("Built-in skill sets can't be renamed")
        handle as UserSkillSetHandle

        handle.replaceStoredFit {
            it.copy(name = name)
        }
    }


    /**
     * Updates the given skill set's EVE SSO tokens.
     */
    suspend fun setEveSsoTokens(handle: SkillSetHandle, eveSsoTokens: EveSsoTokens) {
        if (!handle.isCharacterSkillSet)
            throw IllegalArgumentException("Only character skill sets have EVE SSO tokens")
        handle as UserSkillSetHandle

        handle.replaceStoredFit {
            it.copy(eveSsoTokens = eveSsoTokens)
        }
    }


    /**
     * Deletes a skill set.
     */
    suspend fun delete(handle: SkillSetHandle) {
        if (handle.isBuiltInSkillSet)
            throw IllegalArgumentException("Built-in skill sets can't be deleted")
        handle as UserSkillSetHandle

        handle.storedSkillSet?.let {
            repo.deleteSkillSets(listOf(it))
        }

        // Change the skill set of all fits that are using the skill set we're removing to the default one
        val usingFits = TheorycrafterContext.fits.handles.filter {
            it.storedFit?.skillSetId == handle.skillSetId
        }
        TheorycrafterContext.fits.setSkillSet(usingFits, null)

        _userHandles.remove(handle)
        userHandleById.remove(handle.skillSetId)

        // Remove the engine skill set, if any
        handle.engineSkillSet?.let { engineSkillSet ->
            fittingEngine.modify {
                engineSkillSet.remove()
            }
        }

        handle.storedSkillSet = null
        handle.engineSkillSet = null
    }


    /**
     * Returns the engine [SkillSet] corresponding to the given [SkillSetHandle], loading it into the engine if needed.
     *
     * If [handle] is `null` returns the engine's default skill set.
     */
    suspend fun engineSkillSetOf(handle: SkillSetHandle?): SkillSet {
        if (handle == null)
            return fittingEngine.defaultSkillSet

        handle.engineSkillSet?.let { return it }

        val engineSkillSet = when (handle) {
            is BuiltInSkillSetHandle ->
                fittingEngine.modify {
                    newSkillSet(handle::levelOfSkill)
                }
            is UserSkillSetHandle -> {
                val storedSkillSet = handle.requireStoredSkillSet()
                fittingEngine.modify {
                    newSkillSet { skillType ->
                        storedSkillSet.levelBySkillId[skillType.itemId] ?: UNKNOWN_SKILL_LEVEL
                    }
                }
            }
        }

        handle.engineSkillSet = engineSkillSet

        return engineSkillSet
    }


    /**
     * Returns the [SkillSetHandle] for the skill set with the given id; `null` if such a skill set is not known.
     */
    fun handleByIdOrNull(id: Int): SkillSetHandle? {
        return builtInHandleById[id] ?: userHandleById[id]
    }


    /**
     * Returns the [SkillSetHandle] for the skill set with the given id.
     */
    fun handleById(id: Int): SkillSetHandle {
        return builtInHandleById[id] ?: userHandleById[id] ?: throw IllegalArgumentException("Unknown skill set id: $id")
    }


    /**
     * Returns the [SkillSetHandle] of the given [FitHandle].
     */
    fun skillSetHandleOfFit(fitHandle: FitHandle): SkillSetHandle? {
        return TheorycrafterContext.fits.storedFitOf(fitHandle).skillSetId?.let { skillSetId ->
            handleById(skillSetId)
        }
    }


    /**
     * Sets the default skill set. A `null` value will reset it to [BuiltInSkillSets.Default].
     */
    suspend fun setDefaultSkillSet(handle: SkillSetHandle?) {
        setEngineDefaultSkillSetLevelsFrom(handle ?: BuiltInSkillSets.Default)
        _defaultSkillSetHandle = handle
    }


    /**
     * Sets the levels of [FittingEngine.defaultSkillSet] to the ones of the given skill set.
     */
    private suspend fun setEngineDefaultSkillSetLevelsFrom(handle: SkillSetHandle) {
        val defaultSkillSet = fittingEngine.defaultSkillSet
        fittingEngine.modify {
            for (skillType in fittingEngine.eveData.skillTypes)
                defaultSkillSet.setLevel(skillType, handle.levelOfSkill(skillType))
        }
    }


    /**
     * Sets the level of a single skill in the given skill set.
     */
    suspend fun setSkillLevel(handle: SkillSetHandle, skillType: SkillType, level: Int) {
        setSkillLevels(handle, listOf(skillType to level))
    }


    /**
     * Sets the levels of multiple skills in the given skill set.
     */
    suspend fun setSkillLevels(handle: SkillSetHandle, skillAndLevelList: Collection<Pair<SkillType, Int>>) {
        if (handle is BuiltInSkillSetHandle)
            throw IllegalArgumentException("Built-in skill sets cannot be edited")
        handle as UserSkillSetHandle

        val engineSkillSetsToModify = buildList {
            addNotNull(handle.engineSkillSet)
            if (handle == _defaultSkillSetHandle)
                add(fittingEngine.defaultSkillSet)
        }

        if (engineSkillSetsToModify.isNotEmpty()) {
            fittingEngine.modify {
                for (skillSet in engineSkillSetsToModify)
                    skillSet.setLevels(skillAndLevelList)
            }
        }

        val updateTime = if (handle.isCharacterSkillSet) System.currentTimeMillis() else null
        val skillIdsAndLevels = skillAndLevelList.map { (skillType, level) -> skillType.itemId to level }
        handle.replaceStoredFit {
            it.copy(
                levelBySkillId = it.levelBySkillId + skillIdsAndLevels,
                lastUpdateTimeUtcMillis = updateTime
            )
        }
    }


}


/**
 * The level we use for skills that have been added after the skill set was created.
 */
private const val UNKNOWN_SKILL_LEVEL = 0


/**
 * A handle to a skill set. To obtain the [SkillSet] this handle is for, use [SkillSetsContext.engineSkillSetOf].
 */
sealed interface SkillSetHandle {


    /**
     * The id of the skill set.
     */
    val skillSetId: Int


    /**
     * The name of the skill set.
     */
    val name: String


    /**
     * Whether the skill set has been deleted.
     */
    val isDeleted: Boolean


    /**
     * Returns the level of the given skill in this skill set.
     */
    fun levelOfSkill(skillType: SkillType): Int


}


/**
 * A handle to a stored/user skill set.
 */
private class UserSkillSetHandle(storedSkillSet: StoredSkillSet): SkillSetHandle {


    /**
     * The [StoredSkillSet]; `null` when it has been deleted.
     */
    var storedSkillSet: StoredSkillSet? by mutableStateOf(storedSkillSet)


    /**
     * The engine [SkillSet].
     */
    var engineSkillSet: SkillSet? = null


    /**
     * Returns the stored skill set, checking whether it has been deleted beforehand.
     */
    fun requireStoredSkillSet(): StoredSkillSet = storedSkillSet ?: error("Skill set has been deleted")


    override val skillSetId: Int
        get() = requireStoredSkillSet().id!!


    override val name: String
        get() = requireStoredSkillSet().name


    override val isDeleted: Boolean by derivedStateOf(structuralEqualityPolicy()) {
        this.storedSkillSet == null
    }


    override fun levelOfSkill(skillType: SkillType): Int {
        return requireStoredSkillSet().levelBySkillId[skillType.itemId] ?: UNKNOWN_SKILL_LEVEL
    }


    override fun toString(): String {
        return storedSkillSet?.name ?: "Deleted skill set"
    }


}


/**
 * A handle to a built-in skill set.
 */
private class BuiltInSkillSetHandle(


    /**
     * The id of the skill set. Built-in skill sets have negative ids.
     */
    override val skillSetId: Int,


    /**
     * The name of the skill set.
     */
    override val name: String,


    /**
     * The function specifying the level of each skill.
     */
    private val skillLevel: (SkillType) -> Int


): SkillSetHandle {


    init {
        if (skillSetId >= 0)
            throw IllegalArgumentException("Built-in skill sets must have negative ids")
    }


    /**
     * The engine [SkillSet].
     */
    var engineSkillSet: SkillSet? = null


    override val isDeleted: Boolean
        get() = false


    override fun levelOfSkill(skillType: SkillType) = skillLevel(skillType)


}


/**
 * Returns whether the handle is to a built-in skill set.
 */
val SkillSetHandle.isBuiltInSkillSet: Boolean
    get() = this is BuiltInSkillSetHandle


/**
 * Returns whether the handle is to a character skill set.
 */
val SkillSetHandle.isCharacterSkillSet: Boolean
    get() = (this is UserSkillSetHandle) && (this.requireStoredSkillSet().ssoTokens != null)


/**
 * The [EveSsoTokens] of the skill set. This must be a character skill set.
 */
val SkillSetHandle.ssoTokens: EveSsoTokens
    get() = (this as UserSkillSetHandle).requireStoredSkillSet().ssoTokens!!


/**
 * The last update time (UTC milliseconds) the skill set's levels have been updated from ESI.
 * This must be a character skill set.
 */
val SkillSetHandle.lastUpdateTimeUtcMillis: Long
    get() = (this as UserSkillSetHandle).requireStoredSkillSet().lastUpdateTimeUtcMillis!!


/**
 * The fitting engine [SkillSet] of the given handle.
 */
private var SkillSetHandle.engineSkillSet: SkillSet?
    get() = when (this) {
        is UserSkillSetHandle -> engineSkillSet
        is BuiltInSkillSetHandle -> engineSkillSet
    }
    set(value) = when (this) {
        is UserSkillSetHandle -> engineSkillSet = value
        is BuiltInSkillSetHandle -> engineSkillSet = value
    }


/**
 * The built-in skill sets.
 */
object BuiltInSkillSets {


    /**
     * The built-in skill set with all skills at level 5.
     */
    val AllLevel5: SkillSetHandle = BuiltInSkillSetHandle(
        skillSetId = -1,
        name = "All Level 5",
        skillLevel = { 5 }
    )


    /**
     * The built-in skill set with all skills at level 5, except for specialization skills, which are at level 4.
     */
    private val SpecializationLevel4: SkillSetHandle = BuiltInSkillSetHandle(
        skillSetId = -2,
        name = "Specialization Level 4",
        skillLevel = { if ("Specialization" in it.name) 4 else 5 }
    )


    /**
     * The built-in skill set with all skills at level 0.
     */
    private val Level0: SkillSetHandle = BuiltInSkillSetHandle(
        skillSetId = -3,
        name = "All Level 0",
        skillLevel = { 0 }
    )


    /**
     * The built-in skill set that is used as the default in the application.
     */
    val Default = AllLevel5


    /**
     * The list of all the built-in skill sets.
     */
    val All = listOf(AllLevel5, SpecializationLevel4, Level0)


}
