local addonName, addonTable = ...
---@type MK
local MK = addonTable.MK
local core = addonTable

local CharacterStore = {}
core.CharacterStore = CharacterStore
MK.CharacterStore = CharacterStore

local CHARACTER_STORE_PATCH_DELAY_SECONDS = 0.05
local CHARACTER_STORE_FULL_BROADCAST_DELAY_SECONDS = 1.25

local function GetCurrentCharacterKey()
    local name = UnitName and UnitName("player") or "Unknown"
    local realm = GetRealmName and GetRealmName() or "Unknown"
    return string.format("%s-%s", name or "Unknown", realm or "Unknown")
end

local function GetLastWeeklyResetTimestamp()
    if C_DateAndTime and C_DateAndTime.GetWeeklyResetStartTime then
        local resetTime = C_DateAndTime.GetWeeklyResetStartTime()
        if type(resetTime) == "number" and resetTime > 0 then
            return resetTime
        end
    end

    if C_DateAndTime and C_DateAndTime.GetSecondsUntilWeeklyReset then
        local secondsUntilReset = C_DateAndTime.GetSecondsUntilWeeklyReset()
        if type(secondsUntilReset) == "number" and secondsUntilReset > 0 then
            return time() + secondsUntilReset - (7 * 24 * 60 * 60)
        end
    end

    local now = time()
    local current = date("*t", now)
    local daysSinceWednesday = (current.wday - 4) % 7
    local resetDate = {
        year = current.year,
        month = current.month,
        day = current.day - daysSinceWednesday,
        hour = 5,
        min = 0,
        sec = 0,
    }
    local resetTime = time(resetDate)
    if now < resetTime then
        resetTime = resetTime - (7 * 24 * 60 * 60)
    end
    return resetTime
end

local function GetMaxCharacterLevel()
    if GetMaxPlayerLevel then
        local ok, maxLevel = pcall(GetMaxPlayerLevel)
        maxLevel = tonumber(maxLevel)
        if ok and maxLevel and maxLevel > 0 then
            return maxLevel
        end
    end

    local globalMaxLevel = tonumber(_G.MAX_PLAYER_LEVEL)
    if globalMaxLevel and globalMaxLevel > 0 then
        return globalMaxLevel
    end

    if type(_G.MAX_PLAYER_LEVEL_TABLE) == "table" then
        local maxLevel = 0
        for _, level in pairs(_G.MAX_PLAYER_LEVEL_TABLE) do
            level = tonumber(level)
            if level and level > maxLevel then
                maxLevel = level
            end
        end
        if maxLevel > 0 then
            return maxLevel
        end
    end

    return nil
end

local function CharacterHasKeystoneData(character)
    return character and (
        (tonumber(character.keyLevel) and tonumber(character.keyLevel) > 0)
        or character.keystoneLink
        or character.hasKeystone
    )
end

local function CharacterHasShareableKeystone(character)
    return CharacterHasKeystoneData(character)
end

local function IsMaxLevelOrLegacyKeystoneCharacter(character)
    if not character then
        return false
    end

    local level = tonumber(character.level)
    local maxLevel = GetMaxCharacterLevel()
    if level and maxLevel then
        return level >= maxLevel
    end

    return level == nil and (
        CharacterHasKeystoneData(character)
        or (tonumber(character.rating) or 0) > 0
    )
end

local function SortCharacterEntriesByName(entries)
    table.sort(entries, function(left, right)
        return ((left.data and left.data.name) or left.key or "") < ((right.data and right.data.name) or right.key or "")
    end)
end

local function SortRemoteCharacterEntries(entries)
    table.sort(entries, function(left, right)
        local leftWarband = left.warbandTag or ""
        local rightWarband = right.warbandTag or ""
        if leftWarband ~= rightWarband then
            return leftWarband < rightWarband
        end

        local leftName = (left.data and left.data.name) or left.key or ""
        local rightName = (right.data and right.data.name) or right.key or ""
        if leftName == rightName then
            return (left.owner or "") < (right.owner or "")
        end

        return leftName < rightName
    end)
end

local function SplitCharacterNameRealm(characterKey, character)
    local name = character and character.name or characterKey
    local realm = character and character.realm or nil

    if type(name) == "string" and name:find("-", 1, true) and (type(realm) ~= "string" or realm == "") then
        local shortName, fullRealm = strsplit("-", name)
        name = shortName or name
        realm = fullRealm
    end

    return name, realm
end

local function TrimCharacterIdentifier(value)
    if type(value) ~= "string" then
        return nil
    end

    local trimmed = value:match("^%s*(.-)%s*$")
    if not trimmed or trimmed == "" then
        return nil
    end

    return trimmed
end

local function NormalizeCharacterIdentifier(value)
    local trimmed = TrimCharacterIdentifier(value)
    return trimmed and trimmed:lower() or nil
end

local function SplitIdentifierNameRealm(identifier)
    identifier = TrimCharacterIdentifier(identifier)
    if not identifier then
        return nil, nil
    end

    local name, realm = identifier:match("^([^-]+)%-(.+)$")
    if name and realm and name ~= "" and realm ~= "" then
        return name, realm
    end

    return identifier, nil
end

local function BuildCharacterIdentifier(name, realm)
    name = TrimCharacterIdentifier(name)
    realm = TrimCharacterIdentifier(realm)
    if not name then
        return nil
    end

    local parsedName, parsedRealm = SplitIdentifierNameRealm(name)
    if parsedRealm then
        return string.format("%s-%s", parsedName, parsedRealm)
    end
    if realm then
        return string.format("%s-%s", parsedName or name, realm)
    end

    return parsedName or name
end

local function AddOrderedIdentifierAlias(aliases, aliasSet, identifier)
    local normalized = NormalizeCharacterIdentifier(identifier)
    if normalized and not aliasSet[normalized] then
        aliasSet[normalized] = true
        aliases[#aliases + 1] = normalized
        return true
    end
    return false
end

local function AddLFGLeaverStoreAliases(aliases, aliasSet, name, realm)
    local identifier = BuildCharacterIdentifier(name, realm)
    if not identifier then
        return false
    end

    local _, parsedRealm = SplitIdentifierNameRealm(identifier)
    local hasFullIdentifier = AddOrderedIdentifierAlias(aliases, aliasSet, identifier)
    if not parsedRealm then
        AddOrderedIdentifierAlias(aliases, aliasSet, name)
    end
    return hasFullIdentifier
end

local function AddLFGLeaverLookupAliases(aliases, aliasSet, characterKey, character)
    local hasFullIdentifier = false
    local fallbackName = nil
    local keyName, keyRealm = SplitIdentifierNameRealm(characterKey)
    if keyName and keyRealm then
        hasFullIdentifier = AddOrderedIdentifierAlias(aliases, aliasSet, string.format("%s-%s", keyName, keyRealm)) or hasFullIdentifier
        fallbackName = keyName
    end

    if type(character) == "table" then
        local name, realm = SplitCharacterNameRealm(characterKey, character)
        local identifier = BuildCharacterIdentifier(name, realm)
        local parsedName, parsedRealm = SplitIdentifierNameRealm(identifier)
        if parsedRealm then
            hasFullIdentifier = AddOrderedIdentifierAlias(aliases, aliasSet, identifier) or hasFullIdentifier
        end
        fallbackName = fallbackName or parsedName or name
    end

    if not hasFullIdentifier then
        AddOrderedIdentifierAlias(aliases, aliasSet, characterKey)
        if type(character) == "table" then
            AddOrderedIdentifierAlias(aliases, aliasSet, character.name)
        end
    elseif fallbackName then
        AddOrderedIdentifierAlias(aliases, aliasSet, fallbackName)
    end
end

local function GetLFGLeaverRecord(owner, characterKey, character)
    local leavers = owner and owner.characterStoreLFGLeavers
    if type(leavers) ~= "table" then
        return nil
    end

    local aliases = {}
    AddLFGLeaverLookupAliases(aliases, {}, characterKey, character)
    for _, alias in ipairs(aliases) do
        local record = leavers[alias]
        if type(record) == "table" then
            return record
        end
    end

    return nil
end

local function CopyCharacterData(character)
    local copy = {}
    for key, value in pairs(character) do
        copy[key] = value
    end
    return copy
end

local function ApplyLFGLeaverOverlay(owner, characterKey, character)
    if type(character) ~= "table" then
        return character
    end

    local record = GetLFGLeaverRecord(owner, characterKey, character)
    if type(record) ~= "table" then
        return character
    end

    local characterData = CopyCharacterData(character)
    characterData.isLeaver = record.isLeaver == true or nil
    characterData.leaverSource = record.source
    characterData.leaverUpdatedAt = record.updatedAt
    return characterData
end

function CharacterStore:Invalidate(owner, reason)
    owner = owner or MK
    owner.characterStoreGeneration = (owner.characterStoreGeneration or 0) + 1
    owner.characterStoreLastInvalidationReason = reason
end

function CharacterStore:BuildEntry(owner, characterKey, character, options)
    options = options or {}
    local characterData = ApplyLFGLeaverOverlay(owner, characterKey, character)
    local entry = {
        key = characterKey,
        owner = options.owner,
        source = options.source or (characterData and characterData.source),
        warbandTag = options.warbandTag,
        warbandSourceName = options.warbandSourceName,
        data = characterData,
    }

    if options.isRosterOnly then
        entry.isRosterOnly = true
    end
    if options.isOffline then
        entry.isOffline = true
    end

    if type(characterData) == "table" then
        entry.hasKeystoneData = CharacterHasKeystoneData(characterData) or nil
        entry.accountTypeQueryName, entry.accountTypeQueryRealm = SplitCharacterNameRealm(characterKey, characterData)
        if owner and owner.GetArmorTypeKey then
            entry.armorTypeKey = owner:GetArmorTypeKey(characterData)
        end
        if owner and owner.NormalizeRoles then
            entry.roles = owner:NormalizeRoles(characterData)
        end
        if owner and owner.GetCharacterRoleAvailability then
            entry.roleAvailability = owner:GetCharacterRoleAvailability(characterData)
        end
        if characterData.mapID then
            entry.dungeonKey = {
                mapID = characterData.mapID,
                dungeonName = characterData.dungeonName,
                keyLevel = characterData.keyLevel,
                keystoneLink = characterData.keystoneLink,
                keyUpgradeTrack = characterData.keyUpgradeTrack,
            }
        end
    end

    return entry
end

function CharacterStore:GetSavedCharacters(owner)
    return owner and owner.db and owner.db.global and owner.db.global.characters or {}
end

function CharacterStore:GetMutableCharacterDataByKey(owner, characterKey)
    if type(characterKey) ~= "string" or characterKey == "" then
        return nil
    end

    local savedCharacters = self:GetSavedCharacters(owner)
    if savedCharacters[characterKey] then
        return savedCharacters[characterKey], "saved"
    end

    local testCharacters = owner and owner.testCharacters
    if testCharacters and testCharacters[characterKey] then
        return testCharacters[characterKey], "test"
    end

    return nil
end

function CharacterStore:GetCharacterDataByKey(owner, characterKey)
    local character, source = self:GetMutableCharacterDataByKey(owner, characterKey)
    if character then
        return character, source
    end

    for ownerKey, ownerData in pairs((owner and owner.groupCharacters) or {}) do
        local characters = ownerData and ownerData.characters
        if characters and characters[characterKey] then
            return characters[characterKey], (characters[characterKey] and characters[characterKey].source) or (ownerData and ownerData.source), ownerKey
        end
    end

    for ownerKey, ownerData in pairs((owner and owner.testGroupCharacters) or {}) do
        local characters = ownerData and ownerData.characters
        if characters and characters[characterKey] then
            return characters[characterKey], (characters[characterKey] and characters[characterKey].source) or (ownerData and ownerData.source) or "test", ownerKey
        end
    end

    return nil
end

function CharacterStore:GetLocalCharacterEntries(owner, includeShownWithoutKeystone)
    local entries = {}
    local lastReset = GetLastWeeklyResetTimestamp()
    local currentCharacterKey = GetCurrentCharacterKey()

    for characterKey, character in pairs(self:GetSavedCharacters(owner)) do
        local hasStoredKeystone = CharacterHasKeystoneData(character)
        local keyConfirmedAfterReset = character and character.lastKeySeenAt and character.lastKeySeenAt >= lastReset
        local shouldIncludeShownWithoutKeystone = includeShownWithoutKeystone
            and owner:IsCharacterShownInGroup(character)
            and IsMaxLevelOrLegacyKeystoneCharacter(character)

        if (hasStoredKeystone and keyConfirmedAfterReset) or shouldIncludeShownWithoutKeystone then
            entries[#entries + 1] = self:BuildEntry(owner, characterKey, character)
        elseif characterKey == currentCharacterKey and owner.DebugLog then
            owner:DebugLog(
                "CharacterStore local filtered current=%s hasStoredKeystone=%s keyConfirmedAfterReset=%s lastKeySeenAt=%s lastReset=%s keyLevel=%s mapID=%s hasKeystone=%s link=%s showInGroup=%s",
                tostring(characterKey),
                tostring(hasStoredKeystone),
                tostring(keyConfirmedAfterReset),
                tostring(character and character.lastKeySeenAt),
                tostring(lastReset),
                tostring(character and character.keyLevel),
                tostring(character and character.mapID),
                tostring(character and character.hasKeystone),
                tostring(character and character.keystoneLink),
                tostring(character and owner:IsCharacterShownInGroup(character))
            )
        end
    end

    for characterKey, character in pairs((owner and owner.testCharacters) or {}) do
        entries[#entries + 1] = self:BuildEntry(owner, characterKey, character, {
            source = "test",
        })
    end

    SortCharacterEntriesByName(entries)
    return entries
end

function CharacterStore:GetWarbandCharacterEntries(owner)
    local currentKey = GetCurrentCharacterKey()
    local entries = {}

    for characterKey, character in pairs(self:GetSavedCharacters(owner)) do
        if characterKey ~= currentKey and IsMaxLevelOrLegacyKeystoneCharacter(character) then
            entries[#entries + 1] = self:BuildEntry(owner, characterKey, character)
        end
    end

    for characterKey, character in pairs((owner and owner.testCharacters) or {}) do
        if characterKey ~= currentKey and IsMaxLevelOrLegacyKeystoneCharacter(character) then
            entries[#entries + 1] = self:BuildEntry(owner, characterKey, character, {
                source = "test",
            })
        end
    end

    SortCharacterEntriesByName(entries)
    return entries
end

function CharacterStore:GetRemoteCharacterEntries(owner)
    local entries = {}
    local lastReset = GetLastWeeklyResetTimestamp()

    local function addRemoteOwner(ownerKey, ownerData, sourceOverride)
        local characters = ownerData and ownerData.characters or {}
        local warbandTag = ownerData and ownerData.warbandTag or ""
        for characterKey, character in pairs(characters) do
            local hasStoredKeystone = CharacterHasKeystoneData(character)
            local keyConfirmedAfterReset = character and character.lastKeySeenAt and character.lastKeySeenAt >= lastReset
            if sourceOverride == "test" or (hasStoredKeystone and keyConfirmedAfterReset) or owner:IsCharacterShownInGroup(character) then
                entries[#entries + 1] = self:BuildEntry(owner, characterKey, character, {
                    owner = ownerKey,
                    source = sourceOverride or (character and character.source) or (ownerData and ownerData.source),
                    warbandTag = warbandTag,
                })
            end
        end
    end

    for ownerKey, ownerData in pairs((owner and owner.groupCharacters) or {}) do
        addRemoteOwner(ownerKey, ownerData)
    end

    for ownerKey, ownerData in pairs((owner and owner.testGroupCharacters) or {}) do
        addRemoteOwner(ownerKey, ownerData, "test")
    end

    SortRemoteCharacterEntries(entries)
    return entries
end

function CharacterStore:GetDungeonViewCharacters(owner)
    local entries = {}
    for _, characterEntry in ipairs(self:GetLocalCharacterEntries(owner, false)) do
        if not owner:IsCharacterHidden(characterEntry.data) then
            entries[#entries + 1] = characterEntry
        end
    end

    if owner.IsInGroupForComm and owner:IsInGroupForComm() then
        for _, characterEntry in ipairs(self:GetRemoteCharacterEntries(owner)) do
            entries[#entries + 1] = characterEntry
        end
    end

    return entries
end

function CharacterStore:GetDungeonKeystoneEntries(owner)
    local entries = {}
    for _, characterEntry in ipairs(self:GetDungeonViewCharacters(owner)) do
        local character = characterEntry.data or {}
        if character.mapID and CharacterHasKeystoneData(character) then
            entries[#entries + 1] = characterEntry
        end
    end
    return entries
end

function CharacterStore:GetCurrentCharacterCommData(owner)
    local current = self:GetSavedCharacters(owner)[GetCurrentCharacterKey()]
    if not current then
        return nil
    end

    local shown = owner:IsCharacterShownInGroup(current)
    return {
        name = current.name,
        class = current.class,
        realm = current.realm,
        level = current.level,
        specID = current.specID,
        specName = current.specName,
        specIcon = current.specIcon,
        keyLevel = current.keyLevel,
        mapID = current.mapID,
        dungeonName = current.dungeonName,
        keystoneLink = current.keystoneLink,
        keyUpgradeTrack = current.keyUpgradeTrack,
        hasKeystone = current.hasKeystone,
        lastKeySeenAt = current.lastKeySeenAt,
        rating = current.rating,
        bestRuns = current.bestRuns,
        showInGroup = shown,
        hide = not shown,
        roles = current.roles,
        source = "mk",
        updatedAt = current.lastSeen,
    }
end

function CharacterStore:GetSharableCharacters(owner)
    local sharable = {}

    for _, entry in ipairs(self:GetLocalCharacterEntries(owner, true)) do
        local character = entry.data
        if owner:IsCharacterShownInGroup(character) then
            sharable[entry.key] = {
                name = character.name,
                class = character.class,
                realm = character.realm,
                level = character.level,
                specID = character.specID,
                specName = character.specName,
                specIcon = character.specIcon,
                keyLevel = character.keyLevel,
                mapID = character.mapID,
                dungeonName = character.dungeonName,
                keystoneLink = character.keystoneLink,
                keyUpgradeTrack = character.keyUpgradeTrack,
                hasKeystone = character.hasKeystone,
                lastKeySeenAt = character.lastKeySeenAt,
                rating = character.rating,
                bestRuns = character.bestRuns,
                showInGroup = true,
                hide = false,
                roles = character.roles,
                source = "mk",
                updatedAt = character.lastSeen,
            }
        end
    end

    local currentKey = GetCurrentCharacterKey()
    local current = self:GetSavedCharacters(owner)[currentKey]
    if owner:IsCharacterShownInGroup(current) and CharacterHasShareableKeystone(current) then
        sharable[currentKey] = self:GetCurrentCharacterCommData(owner)
    end

    return sharable
end

function CharacterStore:UpdateCharacterSetting(owner, characterKey, field, value)
    local character, characterSource = self:GetMutableCharacterDataByKey(owner, characterKey)
    if not character then
        return false
    end

    if character[field] == value then
        return false, character
    end

    character[field] = value
    if field == "showInGroup" then
        character.hide = not value
    end

    self:Invalidate(owner, "setting:" .. tostring(field))
    if characterSource ~= "test" then
        owner:QueueCharacterPatchBroadcast(CHARACTER_STORE_PATCH_DELAY_SECONDS)
        owner:QueueBroadcastUpdate(CHARACTER_STORE_FULL_BROADCAST_DELAY_SECONDS)
    end

    local shouldSkipImmediateRefresh = owner.IsMainWindowShown
        and owner:IsMainWindowShown()
        and owner.activeTab == "characters"

    if not shouldSkipImmediateRefresh then
        owner:RefreshMainWindow()
    end

    return true, character
end

function CharacterStore:UpdateCharacterRole(owner, characterKey, roleKey, value)
    local character, characterSource = self:GetMutableCharacterDataByKey(owner, characterKey)
    if not character or not roleKey then
        return false
    end
    if value and owner.IsCharacterRoleAvailable and not owner:IsCharacterRoleAvailable(character, roleKey) then
        return false, owner:NormalizeRoles(character)
    end

    local roles = owner:NormalizeRoles(character)
    local newValue = value and true or nil
    if roles[roleKey] == newValue then
        return false, roles
    end

    roles[roleKey] = newValue
    if not next(roles) then
        roles[roleKey] = true
    end

    character.roles = roles
    character.rolesManuallyConfigured = true
    self:Invalidate(owner, "roles")
    if characterSource ~= "test" then
        owner:QueueCharacterPatchBroadcast(CHARACTER_STORE_PATCH_DELAY_SECONDS)
        owner:QueueBroadcastUpdate(CHARACTER_STORE_FULL_BROADCAST_DELAY_SECONDS)
    end

    local shouldSkipImmediateRefresh = owner.IsMainWindowShown
        and owner:IsMainWindowShown()
        and owner.activeTab == "characters"

    if not shouldSkipImmediateRefresh then
        owner:RefreshMainWindow()
    end

    return true, roles
end

function CharacterStore:UpdateLFGLeaver(owner, name, realm, isLeaver, source)
    owner = owner or MK
    local aliases = {}
    if not AddLFGLeaverStoreAliases(aliases, {}, name, realm) then
        return false
    end

    owner.characterStoreLFGLeavers = type(owner.characterStoreLFGLeavers) == "table" and owner.characterStoreLFGLeavers or {}
    local leavers = owner.characterStoreLFGLeavers
    local leaverValue = isLeaver == true
    local sourceName = source or "BlizzardLFG"
    if not leaverValue then
        local hasExistingRecord = false
        for _, alias in ipairs(aliases) do
            if leavers[alias] ~= nil then
                hasExistingRecord = true
                break
            end
        end
        if not hasExistingRecord then
            return false
        end
    end

    local updatedAt = time and time() or 0
    local changed = false
    for _, alias in ipairs(aliases) do
        local existing = leavers[alias]
        if type(existing) ~= "table"
            or existing.isLeaver ~= leaverValue
            or existing.source ~= sourceName
        then
            leavers[alias] = {
                isLeaver = leaverValue,
                source = sourceName,
                updatedAt = updatedAt,
            }
            changed = true
        else
            existing.updatedAt = updatedAt
        end
    end

    if changed then
        self:Invalidate(owner, "lfgLeaver")
    end

    return changed
end

function MK:GetCharacterDataByKey(characterKey)
    return CharacterStore:GetCharacterDataByKey(self, characterKey)
end

function MK:GetCharacterStore()
    return CharacterStore
end

function MK:GetCharacters(includeShownWithoutKeystone)
    return CharacterStore:GetLocalCharacterEntries(self, includeShownWithoutKeystone)
end

function MK:GetGroupCharacters()
    return CharacterStore:GetRemoteCharacterEntries(self)
end

function MK:GetDungeonKeystoneEntries()
    return CharacterStore:GetDungeonKeystoneEntries(self)
end

function MK:GetCurrentCharacterCommData()
    return CharacterStore:GetCurrentCharacterCommData(self)
end

function MK:GetSharableCharacters()
    return CharacterStore:GetSharableCharacters(self)
end

function MK:UpdateCharacterSetting(characterKey, field, value)
    return CharacterStore:UpdateCharacterSetting(self, characterKey, field, value)
end

function MK:UpdateCharacterRole(characterKey, roleKey, value)
    return CharacterStore:UpdateCharacterRole(self, characterKey, roleKey, value)
end

function MK:UpdateCharacterLFGLeaver(name, realm, isLeaver, source)
    return CharacterStore:UpdateLFGLeaver(self, name, realm, isLeaver, source)
end
