package at.hannibal2.skyhanni.data

import at.hannibal2.skyhanni.SkyHanniMod
import at.hannibal2.skyhanni.config.ConfigUpdaterMigrator
import at.hannibal2.skyhanni.data.FameRanks.getFameRankByNameOrNull
import at.hannibal2.skyhanni.events.BitsUpdateEvent
import at.hannibal2.skyhanni.events.InventoryFullyOpenedEvent
import at.hannibal2.skyhanni.events.LorenzChatEvent
import at.hannibal2.skyhanni.events.ScoreboardChangeEvent
import at.hannibal2.skyhanni.test.command.ErrorManager
import at.hannibal2.skyhanni.utils.CollectionUtils.nextAfter
import at.hannibal2.skyhanni.utils.ItemUtils.getLore
import at.hannibal2.skyhanni.utils.LorenzUtils
import at.hannibal2.skyhanni.utils.NumberUtil.formatInt
import at.hannibal2.skyhanni.utils.SimpleTimeMark
import at.hannibal2.skyhanni.utils.SimpleTimeMark.Companion.asTimeMark
import at.hannibal2.skyhanni.utils.StringUtils.matchFirst
import at.hannibal2.skyhanni.utils.StringUtils.matchMatcher
import at.hannibal2.skyhanni.utils.StringUtils.matches
import at.hannibal2.skyhanni.utils.StringUtils.removeResets
import at.hannibal2.skyhanni.utils.StringUtils.trimWhiteSpace
import at.hannibal2.skyhanni.utils.TimeUtils
import at.hannibal2.skyhanni.utils.repopatterns.RepoPattern
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent
import kotlin.time.Duration.Companion.days

object BitsAPI {
    private val profileStorage get() = ProfileStorageData.profileSpecific?.bits
    private val playerStorage get() = SkyHanniMod.feature.storage

    var bits: Int
        get() = profileStorage?.bits ?: 0
        private set(value) {
            profileStorage?.bits = value
        }
    var currentFameRank: FameRank?
        get() = playerStorage?.currentFameRank?.let { getFameRankByNameOrNull(it) }
        private set(value) {
            if (value != null) {
                playerStorage?.currentFameRank = value.name
            }
        }
    var bitsAvailable: Int
        get() = profileStorage?.bitsAvailable ?: 0
        private set(value) {
            profileStorage?.bitsAvailable = value
        }

    var cookieBuffTime: SimpleTimeMark?
        get() = profileStorage?.boosterCookieExpiryTime?.asTimeMark()
        private set(value) {
            profileStorage?.boosterCookieExpiryTime = value?.toMillis()
        }

    private const val defaultcookiebits = 4800

    private val bitsDataGroup = RepoPattern.group("data.bits")

    // Scoreboard patterns
    val bitsScoreboardPattern by bitsDataGroup.pattern(
        "scoreboard",
        "^Bits: §b(?<amount>[\\d,.]+).*$"
    )

    // Chat patterns
    private val bitsChatGroup = bitsDataGroup.group("chat")

    private val bitsFromFameRankUpChatPattern by bitsChatGroup.pattern(
        "rankup.bits",
        "§eYou gained §3(?<amount>.*) Bits Available §ecompounded from all your §epreviously eaten §6cookies§e! Click here to open §6cookie menu§e!"
    )

    private val fameRankUpPattern by bitsChatGroup.pattern(
        "rankup.rank",
        "[§\\w\\s]+FAME RANK UP (?:§.)+(?<rank>.*)"
    )

    private val boosterCookieAte by bitsChatGroup.pattern(
        "boostercookieate",
        "§eYou consumed a §6Booster Cookie§e!.*"
    )

    // GUI patterns
    private val bitsGuiGroup = bitsDataGroup.group("gui")

    private val bitsAvailableMenuPattern by bitsGuiGroup.pattern(
        "availablemenu",
        "§7Bits Available: §b(?<toClaim>[\\d,]+)(§3.+)?"
    )

    private val fameRankSbMenuPattern by bitsGuiGroup.pattern(
        "sbmenufamerank",
        "§7Your rank: §e(?<rank>.*)"
    )

    /**
     * REGEX-TEST:  §7Duration: §a140d 8h 35m 36s
     */
    private val cookieDurationPattern by bitsGuiGroup.pattern(
        "cookieduration",
        "\\s*§7Duration: §a(?<time>.*)"
    )

    private val noCookieActiveSBMenuPattern by bitsGuiGroup.pattern(
        "sbmenunocookieactive",
        " §7Status: §cNot active!"
    )

    private val noCookieActiveCookieMenuPattern by bitsGuiGroup.pattern(
        "cookiemenucookieactive",
        "(§7§cYou do not currently have a|§cBooster Cookie active!)"
    )

    private val fameRankCommunityShopPattern by bitsGuiGroup.pattern(
        "communityshopfamerank",
        "§7Fame Rank: §e(?<rank>.*)"
    )

    private val bitsGuiNamePattern by bitsGuiGroup.pattern(
        "mainmenuname",
        "^SkyBlock Menu$"
    )

    private val cookieGuiStackPattern by bitsGuiGroup.pattern(
        "mainmenustack",
        "^§6Booster Cookie$"
    )

    private val bitsStackPattern by bitsGuiGroup.pattern(
        "bitsstack",
        "§bBits"
    )

    private val fameRankGuiNamePattern by bitsGuiGroup.pattern(
        "famerankmenuname",
        "^(Community Shop|Booster Cookie)$"
    )

    private val fameRankGuiStackPattern by bitsGuiGroup.pattern(
        "famerankmenustack",
        "^(§aCommunity Shop|§eFame Rank)$"
    )

    @SubscribeEvent
    fun onScoreboardChange(event: ScoreboardChangeEvent) {
        if (!isEnabled()) return
        for (line in event.newList) {
            val message = line.trimWhiteSpace().removeResets()

            bitsScoreboardPattern.matchMatcher(message) {
                val amount = group("amount").formatInt()
                if (amount == bits) return

                if (amount > bits) {
                    val difference = amount - bits
                    bitsAvailable -= difference
                    bits = amount
                    sendBitsGainEvent(difference)
                } else {
                    bits = amount
                    sendBitsSpentEvent()
                }
            }
        }
    }

    @SubscribeEvent
    fun onChat(event: LorenzChatEvent) {
        if (!isEnabled()) return
        val message = event.message.trimWhiteSpace().removeResets()

        bitsFromFameRankUpChatPattern.matchMatcher(message) {
            val amount = group("amount").formatInt()
            bitsAvailable += amount
            sendBitsAvailableGainedEvent()

            return
        }

        fameRankUpPattern.matchMatcher(message) {
            val rank = group("rank")

            currentFameRank = getFameRankByNameOrNull(rank)
                ?: return ErrorManager.logErrorWithData(
                    FameRankNotFoundException(rank),
                    "FameRank $rank not found",
                    "Rank" to rank,
                    "Message" to message,
                    "FameRanks" to FameRanks.fameRanks
                )

            return
        }

        boosterCookieAte.matchMatcher(message) {
            bitsAvailable += (defaultcookiebits * (currentFameRank?.bitsMultiplier ?: return)).toInt()
            val cookieTime = cookieBuffTime
            cookieBuffTime = if (cookieTime == null) SimpleTimeMark.now() + 4.days else cookieTime + 4.days
            sendBitsAvailableGainedEvent()

            return
        }
    }

    @SubscribeEvent
    fun onInventoryOpen(event: InventoryFullyOpenedEvent) {
        if (!isEnabled()) return

        val stacks = event.inventoryItems

        if (bitsGuiNamePattern.matches(event.inventoryName)) {
            val cookieStack = stacks.values.lastOrNull { cookieGuiStackPattern.matches(it.displayName) }

            // If the cookie stack is null, then the player should not have any bits to claim
            if (cookieStack == null) {
                bitsAvailable = 0
                cookieBuffTime = SimpleTimeMark.farPast()
                return
            }

            val lore = cookieStack.getLore()
            lore.matchFirst(bitsAvailableMenuPattern) {
                val amount = group("toClaim").formatInt()
                if (bitsAvailable != amount) {
                    bitsAvailable = amount
                    sendBitsAvailableGainedEvent()

                    val difference = bits - bitsAvailable
                    if (difference > 0) {
                        bits += difference
                    }
                }
            }
            lore.matchFirst(cookieDurationPattern) {
                val duration = TimeUtils.getDuration(group("time"))
                cookieBuffTime = SimpleTimeMark.now() + duration
            }
            lore.matchFirst(noCookieActiveSBMenuPattern) {
                val cookieTime = cookieBuffTime
                if (cookieTime == null || cookieTime.isInFuture()) cookieBuffTime = SimpleTimeMark.farPast()
            }
            return
        }

        if (fameRankGuiNamePattern.matches(event.inventoryName)) {
            val bitsStack = stacks.values.lastOrNull { bitsStackPattern.matches(it.displayName) } ?: return
            val fameRankStack = stacks.values.lastOrNull { fameRankGuiStackPattern.matches(it.displayName) } ?: return
            val cookieStack = stacks.values.lastOrNull { cookieGuiStackPattern.matches(it.displayName) } ?: return

            line@ for (line in fameRankStack.getLore()) {
                fameRankCommunityShopPattern.matchMatcher(line) {
                    val rank = group("rank")

                    currentFameRank = getFameRankByNameOrNull(rank)
                        ?: return ErrorManager.logErrorWithData(
                            FameRankNotFoundException(rank),
                            "FameRank $rank not found",
                            "Rank" to rank,
                            "Lore" to fameRankStack.getLore(),
                            "FameRanks" to FameRanks.fameRanks
                        )

                    continue@line
                }

                fameRankSbMenuPattern.matchMatcher(line) {
                    val rank = group("rank")

                    currentFameRank = getFameRankByNameOrNull(rank)
                        ?: return ErrorManager.logErrorWithData(
                            FameRankNotFoundException(rank),
                            "FameRank $rank not found",
                            "Rank" to rank,
                            "Lore" to fameRankStack.getLore(),
                            "FameRanks" to FameRanks.fameRanks
                        )

                    continue@line
                }
            }

            line@ for (line in bitsStack.getLore()) {
                bitsAvailableMenuPattern.matchMatcher(line) {
                    val amount = group("toClaim").formatInt()
                    if (amount != bitsAvailable) {
                        bitsAvailable = amount
                        sendBitsAvailableGainedEvent()
                    }

                    continue@line
                }
            }

            line@ for (line in cookieStack.getLore()) {
                cookieDurationPattern.matchMatcher(line) {
                    val duration = TimeUtils.getDuration(group("time"))
                    cookieBuffTime = SimpleTimeMark.now().plus(duration)
                }
                if (noCookieActiveCookieMenuPattern.matches(line)) {
                    val nextLine = cookieStack.getLore().nextAfter(line) ?: continue@line
                    if (noCookieActiveCookieMenuPattern.matches(nextLine)) cookieBuffTime = SimpleTimeMark.farPast()
                }
            }
        }
    }

    fun hasCookieBuff() = cookieBuffTime?.isInFuture() ?: false

    private fun sendBitsGainEvent(difference: Int) = BitsUpdateEvent.BitsGain(bits, bitsAvailable, difference).postAndCatch()
    private fun sendBitsSpentEvent() = BitsUpdateEvent.BitsSpent(bits, bitsAvailable).postAndCatch()
    private fun sendBitsAvailableGainedEvent() = BitsUpdateEvent.BitsAvailableGained(bits, bitsAvailable).postAndCatch()

    fun isEnabled() = LorenzUtils.inSkyBlock && profileStorage != null

    @SubscribeEvent
    fun onConfigFix(event: ConfigUpdaterMigrator.ConfigFixEvent) {
        event.move(35, "#profile.bits.bitsToClaim", "#profile.bits.bitsAvailable")
    }

    class FameRankNotFoundException(rank: String) : Exception("FameRank not found: $rank")
}