EbonholdHub.TierData = {}

-- Valid tiers in priority order (highest first). "C" is the implicit default
-- for anything not explicitly assigned; unassigned echoes are treated as C.
local TIER_ORDER = { "S", "A", "B", "C", "F" }
local TIER_RANK  = { S = 5, A = 4, B = 3, C = 2, F = 1 }

-- Weight thresholds used to migrate old echoWeights into tiers on first load.
local MIGRATION_MAP = {
    { min = 71,  tier = "S" },
    { min = 31,  tier = "A" },
    { min = 1,   tier = "B" },
    -- weight == 0 → "F"
}

EbonholdHub.TierData.TIER_ORDER = TIER_ORDER
EbonholdHub.TierData.TIER_RANK  = TIER_RANK

-- ── Quality-aware key helpers ─────────────────────────────────────────────────
-- Composite key format:  "Echo Name|<quality 0-4>"
-- Name-only key format:  "Echo Name"  (legacy; applies to all qualities as fallback)

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

local function ParseTierKey(key)
    local name, q = key:match("^(.+)|(%d+)$")
    if name then return name, tonumber(q) end
    return key, nil
end

EbonholdHub.TierData.ParseTierKey = ParseTierKey

-- ── Internal helpers ─────────────────────────────────────────────────────────

local function GetTiers()
    if EbonholdHubDB and EbonholdHubDB.pendingTiers then
        return EbonholdHubDB.pendingTiers
    end
    local build = EbonholdHub.Build.GetActive()
    if build then
        build.echoTiers = build.echoTiers or {}
        return build.echoTiers
    end
    if EbonholdHubDB then
        EbonholdHubDB.pendingTiers = EbonholdHubDB.pendingTiers or {}
        return EbonholdHubDB.pendingTiers
    end
    return {}
end

local function GetOrders()
    if EbonholdHubDB and EbonholdHubDB.pendingTierOrder then
        return EbonholdHubDB.pendingTierOrder
    end
    local build = EbonholdHub.Build.GetActive()
    if build then
        build.echoTierOrder = build.echoTierOrder or {}
        return build.echoTierOrder
    end
    if EbonholdHubDB then
        EbonholdHubDB.pendingTierOrder = EbonholdHubDB.pendingTierOrder or {}
        return EbonholdHubDB.pendingTierOrder
    end
    return {}
end

local function GetMaxPicksTable()
    if EbonholdHubDB and EbonholdHubDB.pendingMaxPicks then
        return EbonholdHubDB.pendingMaxPicks
    end
    local build = EbonholdHub.Build.GetActive()
    if build then
        build.echoMaxPicks = build.echoMaxPicks or {}
        return build.echoMaxPicks
    end
    if EbonholdHubDB then
        EbonholdHubDB.pendingMaxPicks = EbonholdHubDB.pendingMaxPicks or {}
        return EbonholdHubDB.pendingMaxPicks
    end
    return {}
end

-- ── Public API ────────────────────────────────────────────────────────────────

-- Returns the tier string for an echo name: "S","A","B","C","F".
-- Optional quality (0-4): checks the quality-specific key first, then the
-- name-only key (legacy / applies-to-all), then defaults to "C".
-- Calling without quality behaves exactly as before (name-only lookup).
function EbonholdHub.TierData.GetTier(name, quality)
    if not name or name == "" then return "C" end
    local tiers = GetTiers()
    if quality ~= nil then
        local key = TierKey(name, quality)
        if tiers[key] then return tiers[key] end
    end
    return tiers[name] or "C"
end

-- Returns true if this echo has at least one quality-specific tier assignment.
-- Used to decide whether name-only fallback should be suppressed.
function EbonholdHub.TierData.HasQualitySpecificTier(name)
    if not name or name == "" then return false end
    local tiers = GetTiers()
    local prefix = name .. "|"
    for key in pairs(tiers) do
        if key:sub(1, #prefix) == prefix then return true end
    end
    return false
end

-- Strict tier lookup for Auto Pick: if any quality-specific assignment exists
-- for this echo, only the exact quality key counts — no name-only fallback.
-- This prevents Common from inheriting a tier set only for Rare/Epic.
function EbonholdHub.TierData.GetTierForPick(name, quality)
    if not name or name == "" then return "C" end
    local tiers = GetTiers()
    if quality ~= nil then
        local key = TierKey(name, quality)
        if tiers[key] then return tiers[key] end
        -- If any quality-specific key exists, don't fall back to name-only.
        if EbonholdHub.TierData.HasQualitySpecificTier(name) then return "C" end
    end
    return tiers[name] or "C"
end

-- Assigns a tier. Pass nil or "C" to unassign (removes from explicit tiers).
-- Optional quality: if provided, sets a quality-specific assignment (name|quality).
-- Existing calls without quality continue to work identically.
function EbonholdHub.TierData.SetTier(name, tier, quality)
    if not name or name == "" then return end
    local tiers  = GetTiers()
    local orders = GetOrders()
    local key    = TierKey(name, quality)

    local oldTier = tiers[key] or "C"

    -- Remove from old tier order list
    if oldTier ~= "C" and orders[oldTier] then
        local list = orders[oldTier]
        for i = #list, 1, -1 do
            if list[i] == key then table.remove(list, i) break end
        end
    end

    if not tier or tier == "C" then
        tiers[key] = nil
    else
        tiers[key] = tier
        orders[tier] = orders[tier] or {}
        local found = false
        for _, k in ipairs(orders[tier]) do
            if k == key then found = true break end
        end
        if not found then
            orders[tier][#orders[tier] + 1] = key
        end
    end
end

-- Returns the ordered list of echo names assigned to a tier.
-- For "C", returns nil (C is implicit — not ordered).
function EbonholdHub.TierData.GetOrder(tier)
    if not tier or tier == "C" then return nil end
    local orders = GetOrders()
    orders[tier] = orders[tier] or {}
    return orders[tier]
end

-- Replaces the order list for a tier (used after drag-reorder).
function EbonholdHub.TierData.SetOrder(tier, list)
    if not tier or tier == "C" then return end
    local orders = GetOrders()
    orders[tier] = list
end

-- Inserts an echo into a tier at a specific position index.
-- Optional quality: if provided, uses quality-specific key (name|quality).
function EbonholdHub.TierData.InsertAt(name, tier, pos, quality)
    EbonholdHub.TierData.SetTier(name, tier, quality)  -- handles removal from old tier
    if tier == "C" then return end
    local key    = TierKey(name, quality)
    local orders = GetOrders()
    orders[tier] = orders[tier] or {}
    local list = orders[tier]
    -- Remove if already present (SetTier appended it at end)
    for i = #list, 1, -1 do
        if list[i] == key then table.remove(list, i) end
    end
    -- Insert at desired position
    pos = math.max(1, math.min(pos, #list + 1))
    table.insert(list, pos, key)
end

-- Returns the max times this echo can be picked at its assigned tier per run.
-- 0 = unlimited. Default is 1.
function EbonholdHub.TierData.GetMaxPicks(name)
    if not name or name == "" then return 1 end
    local mp = GetMaxPicksTable()
    if mp[name] == nil then return 1 end
    return mp[name]
end

-- Sets max picks. Pass 0 for unlimited.
function EbonholdHub.TierData.SetMaxPicks(name, n)
    if not name or name == "" then return end
    local mp = GetMaxPicksTable()
    n = math.floor(n or 1)
    if n < 0 then n = 0 end
    if n == 1 then
        mp[name] = nil  -- 1 is default, no need to store
    else
        mp[name] = n
    end
end

-- Moves all quality variants (0-4) of this echo that are currently unassigned
-- (tier C) to F tier.  Qualities already placed in S / A / B / F are untouched.
-- Returns the number of quality variants that were changed.
function EbonholdHub.TierData.TrashUnassigned(name)
    if not name or name == "" then return 0 end
    local changed = 0
    for q = 0, 4 do
        if EbonholdHub.TierData.GetTier(name, q) == "C" then
            EbonholdHub.TierData.SetTier(name, "F", q)
            changed = changed + 1
        end
    end
    return changed
end

-- Returns "S" > "A" > ... rank as a number (higher = better).
function EbonholdHub.TierData.GetRank(tier)
    return TIER_RANK[tier] or TIER_RANK["C"]
end

-- ── Migration ─────────────────────────────────────────────────────────────────

-- Converts old echoWeights (name→int) to the new tier system.
-- Called once on first load when a build has echoWeights but no echoTiers.
function EbonholdHub.TierData.MigrateWeights(build)
    if not build or not build.echoWeights then return end
    if build.echoTiers and next(build.echoTiers) then return end  -- already migrated

    build.echoTiers    = {}
    build.echoTierOrder = {}
    build.echoMaxPicks  = nil

    for name, weight in pairs(build.echoWeights) do
        if type(weight) == "number" then
            local tier = "F"
            if weight == 0 then
                tier = "F"
            else
                for _, entry in ipairs(MIGRATION_MAP) do
                    if weight >= entry.min then
                        tier = entry.tier
                        break
                    end
                end
            end
            if tier ~= "C" then
                build.echoTiers[name] = tier
                build.echoTierOrder[tier] = build.echoTierOrder[tier] or {}
                build.echoTierOrder[tier][#build.echoTierOrder[tier] + 1] = name
            end
        end
    end

    -- Sort each tier's order by original weight descending for sensible defaults
    for tier, list in pairs(build.echoTierOrder) do
        table.sort(list, function(a, b)
            return (build.echoWeights[a] or 0) > (build.echoWeights[b] or 0)
        end)
    end
end

-- ── Pending (edit session) management ─────────────────────────────────────────

-- Called by BuildForm when entering edit mode: copies build tier data to pending buffers.
function EbonholdHub.TierData.BeginEdit(build)
    if not build then return end
    -- Deep-copy tier data into pending buffers
    local function clone(t)
        if type(t) ~= "table" then return t end
        local c = {}
        for k, v in pairs(t) do c[k] = clone(v) end
        return c
    end
    EbonholdHubDB.pendingTiers     = clone(build.echoTiers or {})
    EbonholdHubDB.pendingTierOrder = clone(build.echoTierOrder or {})
    EbonholdHubDB.pendingMaxPicks  = clone(build.echoMaxPicks or {})
    EbonholdHubDB.pendingBundles   = clone(build.echoBundles or {})
end

-- Called by BuildForm.Save: flushes pending buffers back into the build.
function EbonholdHub.TierData.CommitEdit(build)
    if not build then return end
    if EbonholdHubDB.pendingTiers then
        build.echoTiers = EbonholdHubDB.pendingTiers
    end
    if EbonholdHubDB.pendingTierOrder then
        build.echoTierOrder = EbonholdHubDB.pendingTierOrder
    end
    if EbonholdHubDB.pendingMaxPicks then
        build.echoMaxPicks = EbonholdHubDB.pendingMaxPicks
    end
    if EbonholdHubDB.pendingBundles then
        build.echoBundles = EbonholdHubDB.pendingBundles
    end
    EbonholdHub.TierData.ClearPending()
end

-- Called by BuildForm.Cancel: discards pending buffers.
function EbonholdHub.TierData.ClearPending()
    EbonholdHubDB.pendingTiers     = nil
    EbonholdHubDB.pendingTierOrder = nil
    EbonholdHubDB.pendingMaxPicks  = nil
    EbonholdHubDB.pendingBundles   = nil
end

-- Wipes all tier assignments, order, max-picks, and bundles for the active build.
function EbonholdHub.TierData.ClearAll()
    local build = EbonholdHub.Build.GetActive()
    if build then
        build.echoTiers     = {}
        build.echoTierOrder = {}
        build.echoMaxPicks  = {}
        build.echoWeights   = {}   -- also wipe legacy weights so Overview updates
        build.echoBundles   = {}
    end
    EbonholdHub.TierData.ClearPending()
    if EbonholdHubDB then
        EbonholdHubDB.pendingWeights = nil
    end
end

-- Returns echoes assigned to tiers S, A or B for the given build (or active build).
-- Each entry: { name, tier, rank, pos }  sorted best-first.
function EbonholdHub.TierData.GetDesiredEchoes(build)
    build = build or EbonholdHub.Build.GetActive()
    if not build then return {} end

    local tiers  = build.echoTiers    or {}
    local orders = build.echoTierOrder or {}
    local result = {}

    -- Collect explicit S/A/B assignments.
    -- Keys may be bare names (legacy) or "name|quality" (quality-specific).
    local seen = {}
    for _, tierId in ipairs({ "S", "A", "B" }) do
        local order = orders[tierId] or {}
        for pos, key in ipairs(order) do
            if not seen[key] and tiers[key] == tierId then
                seen[key] = true
                local name, quality = ParseTierKey(key)
                result[#result + 1] = {
                    name    = name,
                    quality = quality,
                    tier    = tierId,
                    rank    = TIER_RANK[tierId] or 0,
                    pos     = pos,
                    key     = key,
                }
            end
        end
        -- Also pick up any in tiers table that aren't in the order list
        for key, t in pairs(tiers) do
            if t == tierId and not seen[key] then
                seen[key] = true
                local name, quality = ParseTierKey(key)
                result[#result + 1] = {
                    name    = name,
                    quality = quality,
                    tier    = tierId,
                    rank    = TIER_RANK[tierId] or 0,
                    pos     = 9999,
                    key     = key,
                }
            end
        end
    end

    table.sort(result, function(a, b)
        if a.rank ~= b.rank then return a.rank > b.rank end
        return a.pos < b.pos
    end)
    return result
end

function EbonholdHub.TierData.Init()
end

-- ── Echo Bundles ──────────────────────────────────────────────────────────────
-- Bundles are groups of echoes that together form a higher-priority combo.
-- Stored per build as: build.echoBundles = { {id=N, tier="S"|"A", echoes={"Name1","Name2"}}, ... }
-- When any echo in a bundle is already owned in the current run, all remaining
-- bundle members receive elevated scoring priority (above their base tier).

local function GetBundlesTable()
    if EbonholdHubDB and EbonholdHubDB.pendingBundles ~= nil then
        return EbonholdHubDB.pendingBundles
    end
    local build = EbonholdHub.Build.GetActive()
    if build then
        build.echoBundles = build.echoBundles or {}
        return build.echoBundles
    end
    if EbonholdHubDB then
        EbonholdHubDB.pendingBundles = EbonholdHubDB.pendingBundles or {}
        return EbonholdHubDB.pendingBundles
    end
    return {}
end

function EbonholdHub.TierData.GetBundles()
    return GetBundlesTable()
end

function EbonholdHub.TierData.AddBundle(tier)
    local bundles = GetBundlesTable()
    local maxId = 0
    for _, b in ipairs(bundles) do
        if (b.id or 0) > maxId then maxId = b.id end
    end
    local id = maxId + 1
    bundles[#bundles + 1] = { id = id, tier = tier or "S", echoes = {} }
    return id
end

function EbonholdHub.TierData.RemoveBundle(id)
    local bundles = GetBundlesTable()
    for i = #bundles, 1, -1 do
        if bundles[i].id == id then table.remove(bundles, i); return end
    end
end

-- Adds echoName to the bundle with bundleId.
-- Same echo can appear in multiple bundles simultaneously.
-- Only prevents duplicates within the same bundle.
-- quality is optional. When provided, stores "name|quality" so the bundle
-- boost only fires when that exact quality drops. nil means any quality.
function EbonholdHub.TierData.AddEchoToBundle(bundleId, echoName, quality)
    if not echoName or echoName == "" then return end
    local key = (quality ~= nil) and (echoName .. "|" .. tostring(quality)) or echoName
    local bundles = GetBundlesTable()
    for _, b in ipairs(bundles) do
        if b.id == bundleId then
            for _, n in ipairs(b.echoes) do
                if n == key then return end  -- already in this bundle
            end
            b.echoes[#b.echoes + 1] = key
            return
        end
    end
end

-- Swaps the positions of two bundles in the priority list.
-- Earlier position = higher priority in GetBundleBoost.
function EbonholdHub.TierData.SwapBundles(id1, id2)
    local bundles = GetBundlesTable()
    local i1, i2
    for i, b in ipairs(bundles) do
        if b.id == id1 then i1 = i end
        if b.id == id2 then i2 = i end
    end
    if i1 and i2 then
        bundles[i1], bundles[i2] = bundles[i2], bundles[i1]
    end
end

-- Removes an echo from a specific bundle (by bundleId).
-- Matches by base name regardless of quality suffix (removes all quality variants).
-- If bundleId is nil, removes from ALL bundles (legacy fallback).
function EbonholdHub.TierData.RemoveEchoFromBundle(bundleId, echoName)
    if not echoName or echoName == "" then return end
    local baseName = ParseTierKey(echoName)  -- strip quality if caller passed "name|q"
    local bundles = GetBundlesTable()
    for _, b in ipairs(bundles) do
        if not bundleId or b.id == bundleId then
            for i = #b.echoes, 1, -1 do
                local storedBase = ParseTierKey(b.echoes[i])
                if storedBase == baseName then table.remove(b.echoes, i) end
            end
        end
    end
end

-- Returns the first bundle that contains this echo, or nil.
-- Matches by base name regardless of quality suffix.
function EbonholdHub.TierData.GetBundleForEcho(echoName)
    if not echoName or echoName == "" then return nil end
    local baseName = ParseTierKey(echoName)
    local bundles = GetBundlesTable()
    for _, b in ipairs(bundles) do
        for _, n in ipairs(b.echoes) do
            if ParseTierKey(n) == baseName then return b end
        end
    end
    return nil
end

-- Returns all bundles that contain this echo (an echo can appear in multiple bundles).
-- Matches by base name regardless of quality suffix.
function EbonholdHub.TierData.GetBundlesForEcho(echoName)
    if not echoName or echoName == "" then return {} end
    local baseName = ParseTierKey(echoName)
    local bundles = GetBundlesTable()
    local result = {}
    for _, b in ipairs(bundles) do
        for _, n in ipairs(b.echoes) do
            if ParseTierKey(n) == baseName then
                result[#result + 1] = b
                break
            end
        end
    end
    return result
end

-- Returns bundle boost data if this echo is a non-main member of a bundle whose
-- main echo (echoes[1]) is already owned in the current run.
-- Returns: { tier="S"|"A", bundleId=N } or nil.
--
-- echoName  = base name of the offered echo (no quality suffix)
-- quality   = numeric quality of the offered echo (optional)
--
-- Bundle entries can be "name" (any quality) or "name|q" (specific quality).
-- If a bundle entry has a quality suffix, it only matches that exact quality.
-- Legacy entries without a suffix match any quality (backward-compatible).
function EbonholdHub.TierData.GetBundleBoost(echoName, quality)
    if not echoName or echoName == "" then return nil end
    local bundles = GetBundlesTable()
    local Scoring = EbonholdHub.Scoring
    if not Scoring or not Scoring.GetGrantedCount then return nil end
    for _, b in ipairs(bundles) do
        if #b.echoes >= 2 then
            local mainKey  = b.echoes[1]
            local mainBase = ParseTierKey(mainKey)
            if mainBase ~= echoName then
                local inBundle = false
                for i = 2, #b.echoes do
                    local bBase, bQuality = ParseTierKey(b.echoes[i])
                    if bBase == echoName then
                        -- No quality suffix → match any quality
                        -- Quality suffix → match only that quality
                        if bQuality == nil or bQuality == quality then
                            inBundle = true; break
                        end
                    end
                end
                if inBundle then
                    -- Main echo: owned = any quality of it has been granted
                    if Scoring.GetGrantedCount(mainBase) > 0 then
                        return { tier = b.tier, bundleId = b.id }
                    end
                end
            end
        end
    end
    return nil
end

-- Returns bundle boost data if this echo is the MAIN (trigger) echo of a bundle
-- that hasn't been activated yet (main not yet owned in this run).
-- Ensures the trigger echo itself gets priority so the player picks it and
-- activates the bundle — same protection and scoring as support echoes.
-- echoName = base name; quality = offered quality (nil = any).
function EbonholdHub.TierData.GetMainEchoBoost(echoName, quality)
    if not echoName or echoName == "" then return nil end
    local bundles = GetBundlesTable()
    local Scoring = EbonholdHub.Scoring
    if not Scoring or not Scoring.GetGrantedCount then return nil end
    for _, b in ipairs(bundles) do
        if b.echoes and #b.echoes >= 1 then
            local mainBase, mainQ = ParseTierKey(b.echoes[1])
            if mainBase == echoName then
                -- Quality check: bundle main with specific quality only matches that quality
                if mainQ == nil or mainQ == quality then
                    -- Only boost if main hasn't been picked yet this run
                    if Scoring.GetGrantedCount(mainBase) == 0 then
                        return { tier = b.tier, bundleId = b.id, isMainEcho = true }
                    end
                end
            end
        end
    end
    return nil
end
