EbonholdHub.CharacterEchoes = {}

local TIER_BY_QUALITY = {
    [0] = "B",
    [1] = "B",
    [2] = "A",
    [3] = "S",
    [4] = "S",
}

local function TierForQuality(quality)
    quality = tonumber(quality) or 0
    return TIER_BY_QUALITY[quality] or "B"
end

local function LockedSlotCount()
    return EbonholdHub.Build and EbonholdHub.Build.GetLockedSlotCount() or 6
end

local function EmptyLockedEchoes()
    if EbonholdHub.Build and EbonholdHub.Build.EmptyLockedEchoes then
        return EbonholdHub.Build.EmptyLockedEchoes()
    end
    return { nil, nil, nil, nil, nil, nil }
end

local function ResolveLockedSlots(raw)
    local slots = {}
    if type(raw) ~= "table" then
        return slots
    end

    local maxSlots = LockedSlotCount()
    for index = 1, maxSlots do
        local value = raw[index]
        if type(value) == "number" then
            slots[index] = value
        elseif type(value) == "table" then
            slots[index] = value.spellId or value.id
        end
    end

    if not slots[1] then
        local ordered = {}
        for key, value in pairs(raw) do
            if type(value) == "number" and ProjectEbonhold.PerkDatabase[value] then
                if type(key) == "number" then
                    ordered[key] = value
                else
                    ordered[#ordered + 1] = value
                end
            elseif type(key) == "number" and type(value) == "number" and ProjectEbonhold.PerkDatabase[value] then
                ordered[key] = value
            end
        end
        for index = 1, LockedSlotCount() do
            if ordered[index] then
                slots[index] = ordered[index]
            end
        end
        if not slots[1] then
            local fill = 1
            for index = 1, #ordered do
                if ordered[index] then
                    slots[fill] = ordered[index]
                    fill = fill + 1
                end
            end
        end
    end

    return slots
end

local function ResolveLockedSlotsDiff(targetRaw, baselineRaw)
    local target = ResolveLockedSlots(targetRaw)
    local baseline = ResolveLockedSlots(baselineRaw)
    local out = EmptyLockedEchoes()
    local count = 0

    for index = 1, LockedSlotCount() do
        if target[index] and target[index] ~= baseline[index] then
            out[index] = target[index]
            count = count + 1
        end
    end

    return out, count
end

local function CollectLockedSlots()
    local service = ProjectEbonhold and ProjectEbonhold.PerkService
    if not service or not service.GetLockedPerks then
        return {}
    end
    return ResolveLockedSlots(service.GetLockedPerks())
end

local function GetOwnedQuality(entry, ownedSpellIds)
    local bestQuality = nil

    if entry.spellIds then
        for quality, spellId in pairs(entry.spellIds) do
            if ownedSpellIds[spellId] then
                local q = tonumber(quality) or 0
                if bestQuality == nil or q > bestQuality then
                    bestQuality = q
                end
            end
        end
    end

    if bestQuality == nil and entry.spellId and ownedSpellIds[entry.spellId] then
        local data = ProjectEbonhold.PerkDatabase[entry.spellId]
        bestQuality = data and data.quality or entry.quality or 0
    end

    if bestQuality == nil then
        bestQuality = entry.quality or 0
    end

    return bestQuality
end

local function BestQualityFromRunValue(value)
    local bestQuality = nil
    local bestSpellId = nil

    if type(value) ~= "table" then
        return bestQuality, bestSpellId
    end

    for _, entry in ipairs(value) do
        if type(entry) == "table" then
            local spellId = entry.spellId or entry.id
            local quality = tonumber(entry.quality)
            if quality == nil and spellId and ProjectEbonhold.PerkDatabase[spellId] then
                quality = ProjectEbonhold.PerkDatabase[spellId].quality
            end
            quality = quality or 0
            if bestQuality == nil or quality > bestQuality then
                bestQuality = quality
                bestSpellId = spellId
            end
        end
    end

    if bestQuality == nil and (value.spellId or value.id) then
        bestSpellId = value.spellId or value.id
        local data = bestSpellId and ProjectEbonhold.PerkDatabase[bestSpellId]
        bestQuality = data and data.quality or tonumber(value.quality) or 0
    end

    return bestQuality, bestSpellId
end

local function BuildEchoTiersFromRunTable(granted, baselineGranted, useDiff)
    local EO = EbonholdHub.EchoOwnership
    local baselineSig = baselineGranted and EO.RunEchoSignatureMap(baselineGranted) or {}
    local echoTiers = {}
    local echoCount = 0

    if type(granted) ~= "table" then
        return echoTiers, echoCount
    end

    for key, value in pairs(granted) do
        if type(key) == "string" and key ~= "" and type(value) == "table" then
            local norm = EO.NormalizeName(key)
            local entrySig = EO.RunEchoSignatureMap({ [key] = value })
            local signature = norm and entrySig[norm]
            if not useDiff or not norm or baselineSig[norm] ~= signature then
                local quality, spellId = BestQualityFromRunValue(value)
                local canonical = (spellId and EO.CanonicalNameFromSpellId(spellId)) or key
                if canonical then
                    echoTiers[canonical] = TierForQuality(quality or 0)
                    echoCount = echoCount + 1
                end
            end
        end
    end

    return echoTiers, echoCount
end

function EbonholdHub.CharacterEchoes.CollectEchoData()
    if not ProjectEbonhold or not ProjectEbonhold.PerkDatabase then
        return nil, nil, 0, 0, "ProjectEbonhold is not loaded."
    end

    local EO = EbonholdHub.EchoOwnership
    local ownedLower, ownedGroups, ownedSpellIds = EO.CollectOwnedSets()

    local lockedSlots = CollectLockedSlots()
    local lockedEchoes = EmptyLockedEchoes()
    local lockedCount = 0
    for index = 1, LockedSlotCount() do
        lockedEchoes[index] = lockedSlots[index]
        if lockedEchoes[index] then
            lockedCount = lockedCount + 1
        end
    end

    if not next(ownedLower) and not next(ownedSpellIds) then
        if lockedCount == 0 then
            return {}, lockedEchoes, 0, 0, "No active echoes found on this character."
        end
        return {}, lockedEchoes, 0, lockedCount, nil
    end

    local best = EbonholdHub.EchoTableRows.BuildBestByName()
    local echoTiers = {}
    local echoCount = 0

    for echoName, entry in pairs(best) do
        if EO.IsOwned(echoName, entry, ownedLower, ownedGroups, ownedSpellIds) then
            local quality = GetOwnedQuality(entry, ownedSpellIds)
            echoTiers[echoName] = TierForQuality(quality)
            echoCount = echoCount + 1
        end
    end

    if echoCount == 0 and lockedCount == 0 then
        return {}, lockedEchoes, 0, 0, "Could not match character echoes to this build."
    end

    return echoTiers, lockedEchoes, echoCount, lockedCount, nil
end

function EbonholdHub.CharacterEchoes.CollectInspectEchoData(unit, options)
    unit = unit or "target"
    options = options or {}

    if not ProjectEbonhold or not ProjectEbonhold.PerkDatabase then
        return nil, nil, 0, 0, "ProjectEbonhold is not loaded."
    end

    local service = ProjectEbonhold.PerkService
    if not service or not service.GetGrantedPerks then
        return nil, nil, 0, 0, "PerkService.GetGrantedPerks is not available."
    end

    local granted = options.prefetchedGranted
    local lockedRaw = options.prefetchedLocked
    if granted == nil then
        granted = service.GetGrantedPerks(unit)
        lockedRaw = service.GetLockedPerks and service.GetLockedPerks(unit)
    end

    local isOtherPlayer = unit == "target"
        and UnitExists("target")
        and not UnitIsUnit("target", "player")

    if isOtherPlayer then
        local playerGranted = service.GetGrantedPerks("player")
        local liveGranted = ProjectEbonhold.Perks and ProjectEbonhold.Perks.grantedPerks
        if granted == playerGranted or granted == liveGranted then
            return {}, EmptyLockedEchoes(), 0, 0, "INSPECT_PERKS_PLAYER_CACHE"
        end
        if lockedRaw and service.GetLockedPerks then
            local playerLocked = service.GetLockedPerks("player")
            local liveLocked = ProjectEbonhold.Perks and ProjectEbonhold.Perks.lockedPerks
            if lockedRaw == playerLocked or lockedRaw == liveLocked then
                lockedRaw = nil
            end
        end
    end

    local playerRunBaseline = options.playerRunBaseline
    local useDiff = options.useRunDiff
        and playerRunBaseline
        and isOtherPlayer

    local EO = EbonholdHub.EchoOwnership

    local lockedEchoes
    local lockedCount
    if useDiff and options.playerLockedBaseline then
        lockedEchoes, lockedCount = ResolveLockedSlotsDiff(lockedRaw, options.playerLockedBaseline)
    else
        lockedEchoes = EmptyLockedEchoes()
        lockedCount = 0
        local lockedSlots = ResolveLockedSlots(lockedRaw)
        for index = 1, LockedSlotCount() do
            lockedEchoes[index] = lockedSlots[index]
            if lockedEchoes[index] then
                lockedCount = lockedCount + 1
            end
        end
    end

    if type(granted) ~= "table" or not next(granted) then
        return {}, lockedEchoes, 0, lockedCount, nil
    end

    local echoTiers, echoCount = BuildEchoTiersFromRunTable(granted, playerRunBaseline, useDiff)

    if useDiff and echoCount == 0 and granted ~= service.GetGrantedPerks("player") then
        if EO.TargetRunSnapshotLooksPartial(granted, playerRunBaseline) then
            echoTiers, echoCount = BuildEchoTiersFromRunTable(granted, nil, false)
        end
    end

    return echoTiers, lockedEchoes, echoCount, lockedCount, nil
end

function EbonholdHub.CharacterEchoes.ApplyLockedToForm(lockedEchoes)
    if not lockedEchoes or not EbonholdHub.BuildForm or not EbonholdHub.BuildForm.GetEditingLockedEchoes then
        return 0
    end

    local locked = EbonholdHub.BuildForm.GetEditingLockedEchoes()
    if not locked then return 0 end

    local lockedCount = 0
    for index = 1, LockedSlotCount() do
        locked[index] = lockedEchoes[index]
        if locked[index] then
            lockedCount = lockedCount + 1
        end
    end

    if EbonholdHub.BuildForm.RefreshLockedSlots then
        EbonholdHub.BuildForm.RefreshLockedSlots()
    end

    return lockedCount
end

function EbonholdHub.CharacterEchoes.CopyToBuild()
    local echoTiers, lockedEchoes, echoCount, lockedCount, err = EbonholdHub.CharacterEchoes.CollectEchoData()
    if err then
        return false, err
    end

    local build = EbonholdHub.Build.GetActive()
    if not build then
        return false, "Open or create a build first."
    end

    for echoName, tier in pairs(echoTiers) do
        EbonholdHub.TierData.SetTier(echoName, tier)
    end

    if lockedCount > 0 then
        EbonholdHub.CharacterEchoes.ApplyLockedToForm(lockedEchoes)
    end

    if EbonholdHub.TierView and EbonholdHub.TierView.Refresh then
        EbonholdHub.TierView.Refresh()
    end

    return true, echoCount, lockedCount
end
