EbonholdHub.EchoLoadoutCodec = {}

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

local TIER_RANK = { S = 4, A = 3, B = 2, C = 1, F = 0 }

local QUALITY_SUFFIX_PATTERNS = {
    " %- Common$", " %- Uncommon$", " %- Rare$", " %- Epic$", " %- Legendary$",
    " %- common$", " %- uncommon$", " %- rare$", " %- epic$", " %- legendary$",
}

local function StripQualitySuffix(name)
    if not name or name == "" then return name end
    for _, pattern in ipairs(QUALITY_SUFFIX_PATTERNS) do
        local stripped = name:match("^(.+)" .. pattern)
        if stripped then return stripped end
    end
    return name
end

local function MakeTierKey(name, quality)
    if quality ~= nil then
        return name .. "|" .. tostring(math.floor(tonumber(quality) or 0))
    end
    return name
end

local function LoadoutTierFromCode(code, dbQuality)
    local tier = LOADOUT_TIER_BY_CODE[tonumber(code)]
    if tier then return tier end
    if dbQuality == 3 or dbQuality == 4 then return "S" end
    if dbQuality == 2 then return "A" end
    return "B"
end

local function ResolveCatalogEntry(spellId)
    if not spellId then return nil end

    local dbQuality = nil
    local name = nil

    if ProjectEbonhold and ProjectEbonhold.PerkDatabase then
        local data = ProjectEbonhold.PerkDatabase[spellId]
        if data then
            dbQuality = data.quality
            if data.comment and data.comment ~= "" then
                name = StripQualitySuffix(data.comment)
            end
        end
    end

    if not name and EbonholdHub.EchoOwnership and EbonholdHub.EchoOwnership.CanonicalNameFromSpellId then
        name = EbonholdHub.EchoOwnership.CanonicalNameFromSpellId(spellId)
    end

    if not name and EbonholdHub.EchoTableRows and EbonholdHub.EchoTableRows.BuildBestByName then
        local best = EbonholdHub.EchoTableRows.BuildBestByName()
        for echoName, entry in pairs(best) do
            if entry.spellId == spellId then
                name = echoName
                dbQuality = dbQuality or entry.quality
                break
            end
            for quality, sid in pairs(entry.spellIds or {}) do
                if sid == spellId then
                    name = echoName
                    dbQuality = dbQuality or quality
                    break
                end
            end
            if name then break end
        end
    end

    if not name and GetSpellInfo then
        local spellName = GetSpellInfo(spellId)
        if spellName and spellName ~= "" then
            name = StripQualitySuffix(spellName)
        end
    end

    if not name or name == "" then
        return nil
    end

    return {
        name = name,
        quality = dbQuality or 0,
        spellId = spellId,
    }
end

local function RemoveTierKeyFromOrder(echoTierOrder, tierKey, tierId)
    local list = echoTierOrder[tierId]
    if not list then return end
    for index = #list, 1, -1 do
        if list[index] == tierKey then
            table.remove(list, index)
            break
        end
    end
end

local function AssignEchoTier(echoTiers, echoTierOrder, tierKey, tier, preserveOrder)
    local oldTier = echoTiers[tierKey]
    if oldTier and oldTier ~= tier then
        RemoveTierKeyFromOrder(echoTierOrder, tierKey, oldTier)
    end

    echoTiers[tierKey] = tier
    echoTierOrder[tier] = echoTierOrder[tier] or {}

    if preserveOrder then
        echoTierOrder[tier][#echoTierOrder[tier] + 1] = tierKey
        return
    end

    for _, existingKey in ipairs(echoTierOrder[tier]) do
        if existingKey == tierKey then return end
    end
    echoTierOrder[tier][#echoTierOrder[tier] + 1] = tierKey
end

-- EWL1:CLASS:spellId:flag,spellId:flag,...
-- flag=1 = locked echo (goes into locked echo slots 1..6)
-- flag=0 = tier list echo (quality-based tier: Epic->S, Rare->A, else->B)
local function ParseEWL(text)
    local version, rest = text:match("^(EWL%d+):(.+)$")
    if not version or not rest then
        return nil, "Not an EWL string."
    end

    local classToken, dataPart = rest:match("^([A-Z_]+):(.+)$")
    if not classToken then
        return nil, "Invalid EWL format (expected EWL1:CLASS:spellId:flag,...)."
    end
    classToken = strupper(classToken)

    local echoTiers           = {}
    local echoTierOrder       = { S = {}, A = {}, B = {}, C = {}, F = {}, pool = {} }
    local loadoutEchoSpellIds = {}
    local lockedEchoes        = {}   -- array of spellId, max 6
    local entries             = {}
    local skipped             = 0
    local unresolved          = 0
    local maxLocked           = (EbonholdHub.Build and EbonholdHub.Build.GetLockedSlotCount)
                                    and EbonholdHub.Build.GetLockedSlotCount() or 6

    for token in dataPart:gmatch("[^,]+") do
        local spellIdStr, flagStr = token:match("^(%d+):(%d+)$")
        local spellId = tonumber(spellIdStr)
        local flag    = tonumber(flagStr)

        if not spellId or spellId < 200000 then
            skipped = skipped + 1
        elseif flag == 1 then
            -- Locked echo slot
            if #lockedEchoes < maxLocked then
                lockedEchoes[#lockedEchoes + 1] = spellId
            end
        else
            -- flag=0: tier list echo
            local catalog = ResolveCatalogEntry(spellId)
            if not catalog then
                unresolved = unresolved + 1
            else
                local q    = catalog.quality or 0
                local tier = (q >= 3) and "S" or (q >= 2) and "A" or "B"
                local tierKey = MakeTierKey(catalog.name, q)
                entries[#entries + 1] = {
                    spellId  = spellId,
                    quality  = q,
                    name     = catalog.name,
                    tier     = tier,
                    tierKey  = tierKey,
                }
                AssignEchoTier(echoTiers, echoTierOrder, tierKey, tier, true)
                loadoutEchoSpellIds[tierKey] = spellId
            end
        end
    end

    if #entries == 0 and #lockedEchoes == 0 then
        return nil, "No echo entries found in EWL string."
    end

    local classDisplay = classToken:sub(1, 1) .. strlower(classToken:sub(2))

    return {
        version              = version,
        class                = classToken,
        title                = classDisplay .. " Wishlist",
        entries              = entries,
        echoTiers            = echoTiers,
        echoTierOrder        = echoTierOrder,
        loadoutEchoSpellIds  = loadoutEchoSpellIds,
        lockedEchoes         = lockedEchoes,
        echoCount            = #entries,
        lockedCount          = #lockedEchoes,
        skipped              = skipped,
        unresolved           = unresolved,
    }, nil
end

function EbonholdHub.EchoLoadoutCodec.Parse(text)
    text = strtrim(text or ""):gsub("%s+", "")
    if text == "" then
        return nil, "Empty string."
    end

    -- EWL1 format: EWL1:CLASS:spellId:flag,...
    if text:match("^EWL%d+:") then
        return ParseEWL(text)
    end

    local version, rest = text:match("^(EBH%d+):(.+)$")
    if not version or not rest then
        return nil, "Not an Echo Journal loadout (expected EBH1:... or EWL1:...)."
    end

    local entriesPart, classToken, title = rest:match("^(.+):([A-Z_]+):(.+)$")
    if not entriesPart then
        return nil, "Invalid loadout format (expected EBH1:entries:CLASS:Name)."
    end

    classToken = strupper(classToken)
    title = strtrim(title)
    if title == "" then
        title = "Imported Loadout"
    end

    local entries = {}
    local echoTiers = {}
    local echoTierOrder = { S = {}, A = {}, B = {}, C = {}, F = {}, pool = {} }
    local loadoutEchoSpellIds = {}
    local skipped = 0
    local unresolved = 0

    for token in entriesPart:gmatch("[^,]+") do
        local spellId, loadoutCode, stack = token:match("^(%d+)%.(%d+)%.(%d+)$")
        spellId = tonumber(spellId)
        loadoutCode = tonumber(loadoutCode)
        stack = tonumber(stack)
        if not spellId or spellId < 200000 then
            skipped = skipped + 1
        else
            local catalog = ResolveCatalogEntry(spellId)
            if not catalog then
                unresolved = unresolved + 1
            else
                local tier = LoadoutTierFromCode(loadoutCode, catalog.quality)
                local tierKey = MakeTierKey(catalog.name, catalog.quality)
                entries[#entries + 1] = {
                    spellId = spellId,
                    quality = catalog.quality,
                    stack = stack or 1,
                    name = catalog.name,
                    tier = tier,
                    tierKey = tierKey,
                }
                AssignEchoTier(echoTiers, echoTierOrder, tierKey, tier, true)
                loadoutEchoSpellIds[tierKey] = spellId
            end
        end
    end

    if #entries == 0 then
        return nil, "No valid echo entries in loadout string."
    end

    return {
        version = version,
        class = classToken,
        title = title,
        entries = entries,
        echoTiers = echoTiers,
        echoTierOrder = echoTierOrder,
        loadoutEchoSpellIds = loadoutEchoSpellIds,
        echoCount = #entries,
        skipped = skipped,
        unresolved = unresolved,
    }, nil
end

function EbonholdHub.EchoLoadoutCodec.ImportBuild(text)
    local parsed, err = EbonholdHub.EchoLoadoutCodec.Parse(text)
    if not parsed then
        return nil, err
    end

    if not EbonholdHub.Build or not EbonholdHub.Build.NewObject then
        return nil, "Build module not loaded."
    end

    local comments = "Imported from loadout string (" .. parsed.version .. ")."
    if parsed.skipped and parsed.skipped > 0 then
        comments = comments .. " Skipped " .. parsed.skipped .. " tokens."
    end
    if parsed.unresolved and parsed.unresolved > 0 then
        comments = comments .. " Unresolved: " .. parsed.unresolved .. " spell IDs."
    end

    -- Use lockedEchoes from parsed data when available (EWL format), else empty slots
    local lockedEchoes
    if parsed.lockedEchoes and #parsed.lockedEchoes > 0 then
        lockedEchoes = EbonholdHub.Build.NormalizeLockedEchoes(parsed.lockedEchoes)
    else
        lockedEchoes = EbonholdHub.Build.EmptyLockedEchoes()
    end

    local build = EbonholdHub.Build.NewObject({
        title = parsed.title,
        class = parsed.class or EbonholdHub.Build.PlayerClassToken(),
        spec = 1,
        comments = comments,
        lockedEchoes = lockedEchoes,
        echoTiers = parsed.echoTiers,
        echoTierOrder = parsed.echoTierOrder,
        echoMaxPicks = {},
        loadoutEchoSpellIds = parsed.loadoutEchoSpellIds,
        affixLoadout = {},
        equipmentLoadout = {},
        talents = {},
        talentOrder = {},
        settings = EbonholdHub.Build.DefaultSettings(),
    })

    EbonholdHub.Build.EnsureSettings(build)
    return build, parsed.echoCount
end

function EbonholdHub.EchoLoadoutCodec.IsLoadoutString(text)
    text = strtrim(text or "")
    return text:match("^EBH%d+:") ~= nil or text:match("^EWL%d+:") ~= nil
end
