local _, ns = ...
local L = ns.L or {}

local CustomTasks = {}
ns.CustomTasks = CustomTasks

local TRACKER_DEFAULTS = {
    shown = false,
    locked = false,
    scalePercent = 100,
    opacityPercent = 90,
    background = true,
    styleKey = "frame",
    fontKey = "normal",
}

local TRACKER_STYLE_KEYS = {
    frame = true,
    rows = true,
    text = true,
}

local SECTION_BUILDERS = {
    {
        key = "pve_weekly",
        label = function()
            return string.format("%s / %s", L.MAIN_TAB_PVE or "PvE", L.SUBTAB_PVE_WEEKLY or "Weekly")
        end,
        builder = "BuildWeeklyListView",
    },
    {
        key = "pve_daily",
        label = function()
            return string.format("%s / %s", L.MAIN_TAB_PVE or "PvE", L.SUBTAB_PVE_DAILY or "Daily")
        end,
        builder = "BuildDailyListView",
    },
    {
        key = "pve_events",
        label = function()
            return string.format("%s / %s", L.MAIN_TAB_PVE or "PvE", L.SUBTAB_PVE_EVENTS or "Events")
        end,
        builder = "BuildEventsListView",
    },
    {
        key = "pve_world",
        label = function()
            return string.format("%s / %s", L.MAIN_TAB_PVE or "PvE", L.SUBTAB_PVE_WORLD or "World")
        end,
        builder = "BuildWorldListView",
    },
    {
        key = "pve_delves",
        label = function()
            return string.format("%s / %s", L.MAIN_TAB_PVE or "PvE", L.SUBTAB_PVE_DELVES or "Delves")
        end,
        builder = "BuildDelvesListView",
    },
    {
        key = "pve_prey",
        label = function()
            return string.format("%s / %s", L.MAIN_TAB_PVE or "PvE", L.SUBTAB_PVE_PREY or "Prey")
        end,
        builder = "BuildPreyView",
        prey = true,
    },
    {
        key = "pvp_weekly",
        label = function()
            return string.format("%s / %s", L.MAIN_TAB_PVP or "PvP", L.SUBTAB_PVP_WEEKLY or "Weekly")
        end,
        builder = "BuildPvpWeeklyListView",
    },
    {
        key = "systems_professions",
        label = function()
            return string.format("%s / %s", L.MAIN_TAB_SYSTEMS or "Systems", L.SUBTAB_SYSTEMS_PROFESSIONS or "Professions")
        end,
        builder = "BuildProfessionWeeklyListView",
    },
}

local function shallowCopyList(list)
    local copy = {}
    for _, value in ipairs(type(list) == "table" and list or {}) do
        copy[#copy + 1] = value
    end
    return copy
end

local function normalizeTaskID(taskID)
    taskID = tostring(taskID or "")
    if taskID == "" then
        return nil
    end

    return taskID
end

local function normalizeToken(value)
    value = string.lower(tostring(value or ""))
    value = value:gsub("[^%w]+", "_")
    value = value:gsub("^_+", ""):gsub("_+$", "")
    if value == "" then
        value = "row"
    end

    return value
end

local function normalizeStore(store)
    if type(store) ~= "table" then
        store = {}
    end

    store.items = type(store.items) == "table" and store.items or {}
    store.order = type(store.order) == "table" and store.order or {}

    local cleanOrder = {}
    local seen = {}

    for _, rawTaskID in ipairs(store.order) do
        local taskID = normalizeTaskID(rawTaskID)
        if taskID and store.items[taskID] == true and not seen[taskID] then
            cleanOrder[#cleanOrder + 1] = taskID
            seen[taskID] = true
        end
    end

    for rawTaskID, selected in pairs(store.items) do
        local taskID = normalizeTaskID(rawTaskID)
        if taskID and selected == true then
            if rawTaskID ~= taskID then
                store.items[rawTaskID] = nil
                store.items[taskID] = true
            end
            if not seen[taskID] then
                cleanOrder[#cleanOrder + 1] = taskID
                seen[taskID] = true
            end
        else
            store.items[rawTaskID] = nil
        end
    end

    store.order = cleanOrder
    return store
end

local function normalizeTracker(tracker)
    if type(tracker) ~= "table" then
        tracker = {}
    end

    tracker.shown = tracker.shown == true
    tracker.locked = tracker.locked == true
    tracker.scalePercent = math.max(70, math.min(140, math.floor(tonumber(tracker.scalePercent) or TRACKER_DEFAULTS.scalePercent)))
    tracker.opacityPercent = math.max(35, math.min(100, math.floor(tonumber(tracker.opacityPercent) or TRACKER_DEFAULTS.opacityPercent)))
    if not TRACKER_STYLE_KEYS[tracker.styleKey] then
        tracker.styleKey = tracker.background == false and "rows" or TRACKER_DEFAULTS.styleKey
    end
    tracker.background = tracker.styleKey == "frame"
    tracker.fontKey = (tracker.fontKey == "compact" or tracker.fontKey == "large") and tracker.fontKey or TRACKER_DEFAULTS.fontKey
    tracker.point = type(tracker.point) == "table" and tracker.point or nil

    return tracker
end

local function ensureCustomTasksDB()
    if type(ns.db) ~= "table" then
        return nil
    end

    ns.db.customTasks = type(ns.db.customTasks) == "table" and ns.db.customTasks or {}
    local db = ns.db.customTasks

    db.global = normalizeStore(db.global)
    db.characters = type(db.characters) == "table" and db.characters or {}
    db.meta = type(db.meta) == "table" and db.meta or {}
    db.tracker = normalizeTracker(db.tracker)

    for characterKey, store in pairs(db.characters) do
        if type(characterKey) ~= "string" or characterKey == "" or type(store) ~= "table" then
            db.characters[characterKey] = nil
        else
            db.characters[characterKey] = normalizeStore(store)
            db.characters[characterKey].useGlobal = db.characters[characterKey].useGlobal == true
        end
    end

    return db
end

local function getCharacterKey(keyOrCharacter)
    if type(keyOrCharacter) == "table" then
        keyOrCharacter = keyOrCharacter.key
    end

    if type(keyOrCharacter) == "string" and keyOrCharacter ~= "" then
        return keyOrCharacter
    end

    if ns.GetSelectedCharacter then
        local selected = ns:GetSelectedCharacter()
        if type(selected) == "table" and type(selected.key) == "string" and selected.key ~= "" then
            return selected.key
        end
    end

    if ns.GetCurrentCharacterInfo then
        local current = ns:GetCurrentCharacterInfo()
        if type(current) == "table" and type(current.key) == "string" and current.key ~= "" then
            return current.key
        end
    end

    return nil
end

local function getCharacterStore(characterKey, createIfMissing)
    local db = ensureCustomTasksDB()
    characterKey = getCharacterKey(characterKey)
    if not db or not characterKey then
        return nil
    end

    if type(db.characters[characterKey]) ~= "table" then
        if not createIfMissing then
            return nil
        end
        db.characters[characterKey] = normalizeStore({})
    end

    db.characters[characterKey] = normalizeStore(db.characters[characterKey])
    db.characters[characterKey].useGlobal = db.characters[characterKey].useGlobal == true
    return db.characters[characterKey]
end

local function getActiveStore(characterKey, createIfMissing)
    local db = ensureCustomTasksDB()
    if not db then
        return nil, "character"
    end

    local characterStore = getCharacterStore(characterKey, createIfMissing)
    if characterStore and characterStore.useGlobal == true then
        return db.global, "global"
    end

    return characterStore, "character"
end

local function removeFromOrder(order, taskID)
    for index = #order, 1, -1 do
        if order[index] == taskID then
            table.remove(order, index)
        end
    end
end

local function refreshCustomTaskUI()
    if ns.UI then
        local uiRefreshed = false
        if ns.UI.Refresh then
            ns.UI:Refresh()
            uiRefreshed = ns.UI.frame and ns.UI.frame:IsShown()
        end
        if not uiRefreshed and ns.UI.RefreshCustomTaskTracker then
            ns.UI:RefreshCustomTaskTracker()
        end
    end
end

local function buildView(builderName, character, snapshot)
    local pveView = ns.UIPveView or {}
    local builder = pveView[builderName]
    if type(builder) ~= "function" then
        return nil
    end

    local ok, view = pcall(builder, character, snapshot)
    if ok and type(view) == "table" then
        return view
    end

    return nil
end

local function addDirectCatalogItem(catalog, section, item)
    if type(catalog) ~= "table" or type(section) ~= "table" or type(item) ~= "table" then
        return
    end
    if type(item.id) ~= "string" or item.id == "" then
        return
    end

    item.groupKey = item.groupKey or section.key
    item.groupLabel = item.groupLabel or section.label
    item.sortIndex = item.sortIndex or (#section.items + 1)
    section.items[#section.items + 1] = item
    catalog.index[item.id] = item
end

local function getVaultDisplayedCount(row)
    local thresholds = type(row) == "table" and type(row.thresholds) == "table" and row.thresholds or {}
    local maxThreshold = math.max(0, tonumber(thresholds[#thresholds]) or 0)
    local completed = math.max(0, tonumber(row and row.completedCount) or 0)
    if maxThreshold > 0 then
        completed = math.min(completed, maxThreshold)
    end

    return completed, maxThreshold
end

local function buildVaultTrackerEntry(rowKey, label, snapshot)
    local row = type(snapshot) == "table" and type(snapshot.rows) == "table" and snapshot.rows[rowKey] or nil
    label = label or (row and row.label) or (L.CUSTOM_TASKS_TRACKER_VAULT or "Great Vault")
    if type(row) ~= "table" then
        return {
            key = "focus_vault_" .. tostring(rowKey or "unknown"),
            label = label,
            text = L.CUSTOM_TASKS_UNAVAILABLE_VALUE or "--",
            status = "unknown",
            seen = false,
            completed = false,
            tooltipTitle = label,
            tooltipLines = {
                L.CUSTOM_TASKS_TRACKER_VAULT_MISSING or "No Great Vault snapshot is available for this character yet.",
            },
        }
    end

    local completed, maxThreshold = getVaultDisplayedCount(row)
    local goal = type(row.goal) == "table" and row.goal or nil
    local goalText = goal and (goal.displayCountText or goal.displayText) or nil
    local text = goalText
    if type(text) ~= "string" or text == "" then
        text = maxThreshold > 0 and string.format("%d/%d", completed, maxThreshold) or tostring(completed)
    end

    local missingToMax = tonumber(goal and goal.missingToMax)
    local isComplete = (missingToMax and missingToMax <= 0) or (maxThreshold > 0 and completed >= maxThreshold)
    local tooltipLines = {
        string.format(
            "%s: %s",
            L.VAULT_UNLOCK_LABEL or "Vault slots",
            maxThreshold > 0 and string.format("%d/%d", completed, maxThreshold) or tostring(completed)
        ),
    }
    if goal and goal.displayText then
        tooltipLines[#tooltipLines + 1] = string.format("%s: %s", goal.label or (L.VAULT_GOAL_LABEL or "Max reward"), goal.displayText)
    end
    if row.note and row.note ~= "" then
        tooltipLines[#tooltipLines + 1] = row.note
    end

    return {
        key = "focus_vault_" .. tostring(rowKey or "unknown"),
        label = label,
        text = text,
        status = isComplete and "complete" or "open",
        seen = true,
        completed = isComplete,
        tooltipTitle = label,
        tooltipLines = tooltipLines,
    }
end

local function buildSummaryEntry(key, label, text, completed, tooltipLines, status)
    return {
        key = key,
        label = label,
        text = text,
        status = status or (completed and "complete" or "open"),
        seen = true,
        completed = completed == true,
        tooltipTitle = label,
        tooltipLines = tooltipLines,
    }
end

local function addFocusTrackerCatalogItems(catalog, character, snapshot)
    local section = {
        key = "focus_trackers",
        label = L.CUSTOM_TASKS_TRACKER_GROUP or "Fokus Trackers",
        items = {},
    }

    local vaultLabels = {
        raid = L.CUSTOM_TASKS_TRACKER_VAULT_RAID or "Great Vault: Raid",
        dungeon = L.CUSTOM_TASKS_TRACKER_VAULT_DUNGEON or "Great Vault: Dungeons",
        world = L.CUSTOM_TASKS_TRACKER_VAULT_WORLD or "Great Vault: World / Delves / Prey",
    }
    for _, rowKey in ipairs({ "raid", "dungeon", "world" }) do
        local entry = buildVaultTrackerEntry(rowKey, vaultLabels[rowKey], snapshot)
        addDirectCatalogItem(catalog, section, {
            id = "focus:vault_" .. rowKey,
            label = entry.label,
            entry = entry,
        })
    end

    local delveState = ns.GetLiveDelvesState and ns:GetLiveDelvesState(character, nil) or nil
    local delveSummary = type(delveState) == "table" and type(delveState.summary) == "table" and delveState.summary or nil
    local delveText = type(delveSummary) == "table" and delveSummary.text or nil
    local delveTotal = tonumber(delveSummary and delveSummary.total) or 0
    local delveCompleted = tonumber(delveSummary and delveSummary.completed) or 0
    local delveLabel = L.CUSTOM_TASKS_TRACKER_DELVES or "Delves Progress"
    addDirectCatalogItem(catalog, section, {
        id = "focus:delves_progress",
        label = delveLabel,
        entry = buildSummaryEntry(
            "focus_delves_progress",
            delveLabel,
            delveText or (delveTotal > 0 and string.format("%d/%d", delveCompleted, delveTotal) or (L.CUSTOM_TASKS_UNAVAILABLE_VALUE or "--")),
            delveTotal > 0 and delveCompleted >= delveTotal,
            {
                L.CUSTOM_TASKS_TRACKER_DELVES_TOOLTIP or "Tracks the current Delves summary for this character.",
            },
            delveTotal > 0 and nil or "unknown"
        ),
    })

    local prey = ns.GetLivePreyState and ns:GetLivePreyState(character, nil) or nil
    local preyTotal = math.max(0, tonumber(prey and prey.total) or 0)
    local preyCap = math.max(0, tonumber(prey and prey.totalCap) or 0)
    local preyLabel = L.CUSTOM_TASKS_TRACKER_PREY or "Prey Progress"
    local preyTooltip = {
        L.CUSTOM_TASKS_TRACKER_PREY_TOOLTIP or "Tracks defeated prey targets for the current character.",
    }
    if type(prey) == "table" and type(prey.activeMapName) == "string" and prey.activeMapName ~= "" then
        preyTooltip[#preyTooltip + 1] = string.format(L.PVE_PREY_SUBTITLE_ACTIVE or "Active prey: %s.", prey.activeMapName)
    end
    addDirectCatalogItem(catalog, section, {
        id = "focus:prey_progress",
        label = preyLabel,
        entry = buildSummaryEntry(
            "focus_prey_progress",
            preyLabel,
            preyCap > 0 and string.format("%d/%d", preyTotal, preyCap) or (L.CUSTOM_TASKS_UNAVAILABLE_VALUE or "--"),
            preyCap > 0 and preyTotal >= preyCap,
            preyTooltip,
            preyCap > 0 and nil or "unknown"
        ),
    })

    local knowledge = ns.GetLiveProfessionKnowledgeState and ns:GetLiveProfessionKnowledgeState(character, nil) or nil
    local totalPoints = math.max(0, tonumber(knowledge and knowledge.totalPoints) or 0)
    local professionCount = tonumber(knowledge and knowledge.professionCount) or 0
    local knowledgeLabel = L.CUSTOM_TASKS_TRACKER_PROFESSION_KNOWLEDGE or "Profession Knowledge"
    addDirectCatalogItem(catalog, section, {
        id = "focus:profession_knowledge",
        label = knowledgeLabel,
        entry = buildSummaryEntry(
            "focus_profession_knowledge",
            knowledgeLabel,
            totalPoints > 0 and ("+" .. tostring(totalPoints)) or "0",
            totalPoints <= 0 and professionCount > 0,
            {
                L.CUSTOM_TASKS_TRACKER_PROFESSION_KNOWLEDGE_TOOLTIP or "Tracks unspent profession knowledge points.",
            },
            professionCount <= 0 and "unknown" or (totalPoints > 0 and "warning" or "complete")
        ),
    })

    for index, profession in ipairs(type(knowledge) == "table" and type(knowledge.professions) == "table" and knowledge.professions or {}) do
        local name = profession.name or (L.UNKNOWN or "Unknown")
        local points = math.max(0, tonumber(profession.points) or 0)
        local label = string.format(L.CUSTOM_TASKS_TRACKER_PROFESSION_FORMAT or "Knowledge: %s", name)
        addDirectCatalogItem(catalog, section, {
            id = "focus:profession_knowledge_" .. tostring(profession.skillLineID or profession.professionKey or index),
            label = label,
            entry = buildSummaryEntry(
                "focus_profession_knowledge_" .. tostring(profession.skillLineID or index),
                label,
                points > 0 and ("+" .. tostring(points)) or "0",
                points <= 0,
                {
                    L.CUSTOM_TASKS_TRACKER_PROFESSION_KNOWLEDGE_TOOLTIP or "Tracks unspent profession knowledge points.",
                },
                points > 0 and "warning" or "complete"
            ),
        })
    end

    if #section.items > 0 then
        catalog.groups[#catalog.groups + 1] = section
    end
end

local function getRowLabel(rowData, entry)
    local label = rowData and rowData.label
    if type(label) == "string" and label ~= "" then
        return label
    end

    label = entry and (entry.label or entry.tooltipTitle)
    if type(label) == "string" and label ~= "" then
        return label
    end

    return L.UNKNOWN or "Unknown"
end

local function getRowKey(rowData, entry, index)
    if type(entry) == "table" then
        if type(entry.customTaskKey) == "string" and entry.customTaskKey ~= "" then
            return entry.customTaskKey
        end
        if type(entry.key) == "string" and entry.key ~= "" then
            return entry.key
        end
        local questID = tonumber(entry.questID)
        if questID and questID > 0 then
            return "quest_" .. tostring(questID)
        end
    end

    if type(rowData) == "table" and type(rowData.key) == "string" and rowData.key ~= "" then
        return rowData.key
    end

    return string.format("%s_%d", normalizeToken(getRowLabel(rowData, entry)), tonumber(index) or 1)
end

local function addCatalogItem(catalog, section, rowData, index)
    local entry = type(rowData) == "table" and rowData.entry or nil
    if type(entry) ~= "table" then
        return
    end
    if entry.key == "profession_none" then
        return
    end

    local rowKey = getRowKey(rowData, entry, index)
    local taskID = string.format("%s:%s", section.key, rowKey)
    local label = getRowLabel(rowData, entry)
    local item = {
        id = taskID,
        label = label,
        entry = entry,
        groupKey = section.key,
        groupLabel = section.label,
        sortIndex = index,
    }

    section.items[#section.items + 1] = item
    catalog.index[taskID] = item
end

local function addViewRows(catalog, sourceSection, view)
    local section = {
        key = sourceSection.key,
        label = sourceSection.label(),
        items = {},
    }

    if sourceSection.prey == true then
        local weeklyQuest = type(view) == "table" and type(view.weeklyQuest) == "table" and view.weeklyQuest or nil
        if weeklyQuest then
            addCatalogItem(catalog, section, {
                label = view.weeklyLabel or weeklyQuest.label or weeklyQuest.tooltipTitle or (L.PVE_PREY_WEEKLY_NAME or "A Nightmarish Task"),
                entry = weeklyQuest,
                key = "weekly_quest",
            }, 1)
            local item = section.items[#section.items]
            if item then
                catalog.index[item.id] = nil
                item.id = sourceSection.key .. ":weekly_quest"
                catalog.index[item.id] = item
            end
        end
    else
        for index, rowData in ipairs(type(view) == "table" and view.rows or {}) do
            addCatalogItem(catalog, section, rowData, index)
        end
    end

    if #section.items > 0 then
        catalog.groups[#catalog.groups + 1] = section
    end
end

local function buildCustomTaskCatalog(character, snapshot)
    local catalog = {
        groups = {},
        index = {},
    }

    if type(character) ~= "table" and ns.GetSelectedCharacter then
        character = ns:GetSelectedCharacter()
    end
    if type(character) ~= "table" and ns.GetCurrentCharacterInfo then
        character = ns:GetCurrentCharacterInfo()
    end

    if not snapshot and ns.GetLiveVaultSnapshot then
        snapshot = ns:GetLiveVaultSnapshot(character)
    end

    addFocusTrackerCatalogItems(catalog, character, snapshot)

    for _, section in ipairs(SECTION_BUILDERS) do
        addViewRows(catalog, section, buildView(section.builder, character, snapshot) or {})
    end

    return catalog
end

local function buildUnavailableTask(taskID)
    local db = ensureCustomTasksDB()
    local meta = db and type(db.meta) == "table" and db.meta[taskID] or nil
    local label = type(meta) == "table" and type(meta.label) == "string" and meta.label or taskID

    return {
        id = taskID,
        label = label,
        groupLabel = type(meta) == "table" and meta.groupLabel or (L.CUSTOM_TASKS_UNKNOWN_GROUP or "Unavailable"),
        entry = {
            status = "unknown",
            text = L.CUSTOM_TASKS_UNAVAILABLE_VALUE or "--",
            tooltipTitle = label,
            tooltipLines = {
                L.CUSTOM_TASKS_UNAVAILABLE_TOOLTIP or "This row is not available for the selected character right now.",
            },
        },
    }
end

function ns:EnsureCustomTasksDB()
    return ensureCustomTasksDB()
end

function ns:IsCustomTaskUsingGlobal(characterKey)
    local store = getCharacterStore(characterKey, true)
    return store and store.useGlobal == true or false
end

function ns:SetCustomTaskUseGlobal(characterKey, useGlobal)
    local store = getCharacterStore(characterKey, true)
    if not store then
        return false
    end

    store.useGlobal = useGlobal == true
    refreshCustomTaskUI()
    return true
end

function ns:GetCustomTaskActiveScope(characterKey)
    local _, scope = getActiveStore(characterKey, true)
    return scope
end

function ns:GetCustomTaskIDs(characterKey)
    local store = getActiveStore(characterKey, false)
    return shallowCopyList(store and store.order or {})
end

function ns:IsCustomTaskSelected(taskID, characterKey)
    taskID = normalizeTaskID(taskID)
    if not taskID then
        return false
    end

    local store = getActiveStore(characterKey, false)
    return store and store.items and store.items[taskID] == true or false
end

function ns:SetCustomTaskSelected(taskID, selected, characterKey, metadata)
    taskID = normalizeTaskID(taskID)
    if not taskID then
        return false
    end

    local store = getActiveStore(characterKey, true)
    if not store then
        return false
    end

    if selected == true then
        if store.items[taskID] ~= true then
            store.order[#store.order + 1] = taskID
        end
        store.items[taskID] = true

        local db = ensureCustomTasksDB()
        if db and type(metadata) == "table" then
            db.meta[taskID] = {
                label = metadata.label,
                groupLabel = metadata.groupLabel,
            }
        end
    else
        store.items[taskID] = nil
        removeFromOrder(store.order, taskID)
    end

    normalizeStore(store)
    refreshCustomTaskUI()
    return true
end

function ns:MoveCustomTask(taskID, direction, characterKey)
    taskID = normalizeTaskID(taskID)
    direction = tonumber(direction) or 0
    if not taskID or direction == 0 then
        return false
    end

    local store = getActiveStore(characterKey, true)
    if not store then
        return false
    end

    normalizeStore(store)

    local currentIndex
    for index, storedTaskID in ipairs(store.order or {}) do
        if storedTaskID == taskID then
            currentIndex = index
            break
        end
    end

    if not currentIndex then
        return false
    end

    local targetIndex = currentIndex + (direction < 0 and -1 or 1)
    if targetIndex < 1 or targetIndex > #store.order then
        return false
    end

    store.order[currentIndex], store.order[targetIndex] = store.order[targetIndex], store.order[currentIndex]
    refreshCustomTaskUI()
    return true
end

function ns:CopyGlobalCustomTasksToCharacter(characterKey)
    local db = ensureCustomTasksDB()
    local characterStore = getCharacterStore(characterKey, true)
    if not db or not characterStore then
        return false
    end

    characterStore.items = {}
    characterStore.order = {}
    for _, taskID in ipairs(db.global.order or {}) do
        if db.global.items[taskID] == true then
            characterStore.items[taskID] = true
            characterStore.order[#characterStore.order + 1] = taskID
        end
    end
    characterStore.useGlobal = false
    normalizeStore(characterStore)
    refreshCustomTaskUI()
    return true
end

function ns:SaveCharacterCustomTasksAsGlobal(characterKey)
    local db = ensureCustomTasksDB()
    local characterStore = getCharacterStore(characterKey, true)
    if not db or not characterStore then
        return false
    end

    db.global.items = {}
    db.global.order = {}
    for _, taskID in ipairs(characterStore.order or {}) do
        if characterStore.items[taskID] == true then
            db.global.items[taskID] = true
            db.global.order[#db.global.order + 1] = taskID
        end
    end
    normalizeStore(db.global)
    refreshCustomTaskUI()
    return true
end

function ns:BuildCustomTaskCatalog(character, snapshot)
    return buildCustomTaskCatalog(character, snapshot)
end

function ns:BuildCustomTaskRows(character, snapshot)
    local characterKey = getCharacterKey(character)
    local catalog = buildCustomTaskCatalog(character, snapshot)
    local rows = {}
    local completed = 0
    local ids = self:GetCustomTaskIDs(characterKey)

    for _, taskID in ipairs(ids) do
        local item = catalog.index[taskID]
        if not item then
            item = buildUnavailableTask(taskID)
        end

        rows[#rows + 1] = item
        if item.entry and (item.entry.completed == true or item.entry.status == "complete") then
            completed = completed + 1
        end
    end

    return {
        rows = rows,
        catalog = catalog,
        summary = {
            completed = completed,
            total = #rows,
            text = string.format("%d/%d", completed, #rows),
        },
    }
end

function ns:GetCustomTaskTrackerSettings()
    local db = ensureCustomTasksDB()
    return db and db.tracker or normalizeTracker({})
end

function ns:SetCustomTaskTrackerShown(shown)
    local settings = self:GetCustomTaskTrackerSettings()
    settings.shown = shown == true
    refreshCustomTaskUI()
end

function ns:SetCustomTaskTrackerLocked(locked)
    local settings = self:GetCustomTaskTrackerSettings()
    settings.locked = locked == true
    refreshCustomTaskUI()
end

function ns:SetCustomTaskTrackerBackgroundShown(shown)
    local settings = self:GetCustomTaskTrackerSettings()
    settings.styleKey = shown == true and "frame" or "rows"
    settings.background = settings.styleKey == "frame"
    refreshCustomTaskUI()
end

function ns:CycleCustomTaskTrackerStyle()
    local settings = self:GetCustomTaskTrackerSettings()
    if settings.styleKey == "frame" then
        settings.styleKey = "rows"
    elseif settings.styleKey == "rows" then
        settings.styleKey = "text"
    else
        settings.styleKey = "frame"
    end
    settings.background = settings.styleKey == "frame"
    refreshCustomTaskUI()
end

function ns:SetCustomTaskTrackerScalePercent(value)
    local settings = self:GetCustomTaskTrackerSettings()
    settings.scalePercent = math.max(70, math.min(140, math.floor(tonumber(value) or 100)))
    refreshCustomTaskUI()
end

function ns:SetCustomTaskTrackerOpacityPercent(value)
    local settings = self:GetCustomTaskTrackerSettings()
    settings.opacityPercent = math.max(35, math.min(100, math.floor(tonumber(value) or 90)))
    refreshCustomTaskUI()
end

function ns:CycleCustomTaskTrackerFont()
    local settings = self:GetCustomTaskTrackerSettings()
    if settings.fontKey == "normal" then
        settings.fontKey = "compact"
    elseif settings.fontKey == "compact" then
        settings.fontKey = "large"
    else
        settings.fontKey = "normal"
    end
    refreshCustomTaskUI()
end

function ns:SetCustomTaskTrackerPosition(point, relativePoint, x, y)
    local settings = self:GetCustomTaskTrackerSettings()
    settings.point = {
        point = type(point) == "string" and point or "CENTER",
        relativePoint = type(relativePoint) == "string" and relativePoint or "CENTER",
        x = tonumber(x) or 360,
        y = tonumber(y) or 0,
    }
end

CustomTasks.NormalizeStore = normalizeStore
