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

local CustomTaskView = {}
ns.UICustomTaskView = CustomTaskView

local BACKDROP_TEMPLATE = BackdropTemplateMixin and "BackdropTemplate" or nil

local UIData = ns.UIData or {}
local PREMIUM = UIData.PREMIUM or {}
local THEME = UIData.THEME or {}

local GOLD = THEME.gold or { 0.92, 0.76, 0.24 }
local GOLD_DIM = THEME.goldDim or { 0.40, 0.31, 0.10 }
local GOLD_SOFT = THEME.goldSoft or { 0.66, 0.52, 0.18 }
local METAL = THEME.metal or { 0.10, 0.09, 0.08 }
local METAL_DEEP = THEME.metalDeep or { 0.07, 0.06, 0.055 }
local PARCHMENT = THEME.parchment or { 0.93, 0.89, 0.77 }
local SELECTED = THEME.selected or { 0.26, 0.18, 0.08 }
local SHADOW = THEME.shadow or { 0.01, 0.01, 0.01 }

local TRACKER_WIDTH = 318
local TRACKER_ROW_LIMIT = 12
local ARROW_UP_TEXTURE = PREMIUM.arrowUp or "Interface\\AddOns\\Vaultloom\\Assets\\arrow_up_small.tga"
local ARROW_DOWN_TEXTURE = PREMIUM.arrowDown or "Interface\\AddOns\\Vaultloom\\Assets\\arrow_down_small.tga"

local function callHelper(helpers, key)
    local value = helpers and helpers[key]
    if type(value) == "function" then
        return value
    end

    return function() end
end

local function setTextureColor(texture, color, alpha)
    if not texture or type(color) ~= "table" then
        return
    end

    texture:SetVertexColor(color[1], color[2], color[3], alpha == nil and 1 or alpha)
end

local function setButtonActive(helpers, button, active)
    if not button then
        return
    end

    if helpers and type(helpers.setMiniActionButtonActive) == "function" then
        helpers.setMiniActionButtonActive(button, active == true)
    else
        button:SetAlpha(active and 1 or 0.78)
    end
end

local function getCharacterKey(character)
    if type(character) == "table" and type(character.key) == "string" and character.key ~= "" then
        return character.key
    end

    local current = ns.GetCurrentCharacterInfo and ns:GetCurrentCharacterInfo() or nil
    return current and current.key or nil
end

local function setMiniButtonText(button, text, minWidth, maxWidth)
    if not button then
        return
    end

    if button.SetButtonText then
        button:SetButtonText(text or "", minWidth, maxWidth)
    elseif button.label then
        button.label:SetText(text or "")
    else
        button:SetText(text or "")
    end
end

local function setMiniButtonIcon(button, texture, size, minWidth, maxWidth)
    if not button then
        return
    end

    if button.SetButtonIcon then
        button:SetButtonIcon(texture, size or 12, minWidth or 26, maxWidth or minWidth or 26)
        return
    end

    setMiniButtonText(button, "", minWidth or 26, maxWidth or minWidth or 26)
    if not button.glyphIcon then
        button.glyphIcon = button:CreateTexture(nil, "ARTWORK")
        button.glyphIcon:SetPoint("CENTER", 0, 0)
    end
    button.glyphIcon:SetTexture(texture)
    button.glyphIcon:SetSize(size or 12, size or 12)
    button.glyphIcon:SetVertexColor(1, 1, 1, 1)
    button.glyphIcon:Show()
end

local function bindButtonTooltip(button)
    if not button or button.vlCustomTaskTooltipBound then
        return
    end

    button.vlCustomTaskTooltipBound = true
    button.vlBaseOnEnter = button:GetScript("OnEnter")
    button.vlBaseOnLeave = button:GetScript("OnLeave")
    button:SetScript("OnEnter", function(selfButton)
        if selfButton.vlBaseOnEnter then
            selfButton.vlBaseOnEnter(selfButton)
        end

        if selfButton.vlTooltipTitle or selfButton.vlTooltipText then
            GameTooltip:SetOwner(selfButton, "ANCHOR_CURSOR")
            if selfButton.vlTooltipTitle and selfButton.vlTooltipTitle ~= "" then
                GameTooltip:AddLine(selfButton.vlTooltipTitle, GOLD[1], GOLD[2], GOLD[3], true)
            end
            if selfButton.vlTooltipText and selfButton.vlTooltipText ~= "" then
                GameTooltip:AddLine(selfButton.vlTooltipText, 0.88, 0.88, 0.86, true)
            end
            GameTooltip:Show()
        end
    end)
    button:SetScript("OnLeave", function(selfButton)
        if selfButton.vlBaseOnLeave then
            selfButton.vlBaseOnLeave(selfButton)
        end
        GameTooltip:Hide()
    end)
end

local function styleMenuRow(row)
    if row.SetBackdrop then
        row:SetBackdrop({
            bgFile = "Interface\\Buttons\\WHITE8X8",
            edgeFile = "Interface\\Buttons\\WHITE8X8",
            tile = false,
            edgeSize = 1,
            insets = { left = 1, right = 1, top = 1, bottom = 1 },
        })
        row:SetBackdropColor(METAL_DEEP[1], METAL_DEEP[2], METAL_DEEP[3], 0.48)
        row:SetBackdropBorderColor(GOLD_DIM[1], GOLD_DIM[2], GOLD_DIM[3], 0.34)
    end
end

local function createMenuRow(parent, helpers)
    local createLabel = callHelper(helpers, "createLabel")
    local row = CreateFrame("Button", nil, parent, BACKDROP_TEMPLATE)
    row:SetHeight(24)
    styleMenuRow(row)

    row.box = CreateFrame("Frame", nil, row, BACKDROP_TEMPLATE)
    row.box:SetSize(16, 16)
    row.box:SetPoint("LEFT", 6, 0)
    if row.box.SetBackdrop then
        row.box:SetBackdrop({
            bgFile = "Interface\\Buttons\\WHITE8X8",
            edgeFile = "Interface\\Buttons\\WHITE8X8",
            tile = false,
            edgeSize = 1,
            insets = { left = 1, right = 1, top = 1, bottom = 1 },
        })
        row.box:SetBackdropColor(METAL[1], METAL[2], METAL[3], 0.92)
        row.box:SetBackdropBorderColor(GOLD_DIM[1], GOLD_DIM[2], GOLD_DIM[3], 0.72)
    end

    row.check = row.box:CreateTexture(nil, "ARTWORK")
    row.check:SetSize(18, 18)
    row.check:SetPoint("CENTER", 0, 0)
    row.check:SetTexture("Interface\\Buttons\\UI-CheckBox-Check")
    row.check:SetVertexColor(GOLD[1], GOLD[2], GOLD[3], 1)

    row.label = createLabel(row, "GameFontHighlightSmall", "LEFT")
    row.label:SetPoint("LEFT", row.box, "RIGHT", 8, 0)
    row.label:SetPoint("RIGHT", -8, 0)
    if row.label.SetWordWrap then
        row.label:SetWordWrap(false)
    end
    if row.label.SetNonSpaceWrap then
        row.label:SetNonSpaceWrap(false)
    end

    row:SetScript("OnEnter", function(selfButton)
        if selfButton.SetBackdropBorderColor and not selfButton.isHeader then
            selfButton:SetBackdropBorderColor(GOLD_SOFT[1], GOLD_SOFT[2], GOLD_SOFT[3], 0.76)
        end
        if selfButton.tooltipTitle or selfButton.tooltipLines then
            GameTooltip:SetOwner(selfButton, "ANCHOR_CURSOR_RIGHT")
            if selfButton.tooltipTitle then
                GameTooltip:AddLine(selfButton.tooltipTitle, GOLD[1], GOLD[2], GOLD[3], true)
            end
            for _, line in ipairs(selfButton.tooltipLines or {}) do
                if type(line) == "string" and line ~= "" then
                    GameTooltip:AddLine(line, 0.88, 0.88, 0.86, true)
                end
            end
            GameTooltip:Show()
        end
    end)
    row:SetScript("OnLeave", function(selfButton)
        if selfButton.SetBackdropBorderColor then
            selfButton:SetBackdropBorderColor(GOLD_DIM[1], GOLD_DIM[2], GOLD_DIM[3], selfButton.isHeader and 0 or 0.34)
        end
        GameTooltip:Hide()
    end)

    return row
end

local function setMenuRowHeader(row, text)
    row.isHeader = true
    row.taskID = nil
    row:SetHeight(20)
    row:Disable()
    row.box:Hide()
    row.check:Hide()
    row.label:ClearAllPoints()
    row.label:SetPoint("LEFT", 4, 0)
    row.label:SetPoint("RIGHT", -4, 0)
    row.label:SetText(text or "")
    row.label:SetTextColor(GOLD[1], GOLD[2], GOLD[3], 0.94)
    row.tooltipTitle = nil
    row.tooltipLines = nil
    row:SetScript("OnClick", nil)
    if row.SetBackdropColor then
        row:SetBackdropColor(METAL_DEEP[1], METAL_DEEP[2], METAL_DEEP[3], 0)
        row:SetBackdropBorderColor(GOLD_DIM[1], GOLD_DIM[2], GOLD_DIM[3], 0)
    end
    row:Show()
end

local function setMenuRowItem(row, item, characterKey)
    row.isHeader = false
    row.taskID = item and item.id or nil
    row:SetHeight(24)
    row:Enable()
    row.box:Show()
    row.label:ClearAllPoints()
    row.label:SetPoint("LEFT", row.box, "RIGHT", 8, 0)
    row.label:SetPoint("RIGHT", -8, 0)
    row.label:SetText(item and item.label or "")
    row.label:SetTextColor(PARCHMENT[1], PARCHMENT[2], PARCHMENT[3], 0.94)
    row.tooltipTitle = item and item.label or nil
    row.tooltipLines = item and item.entry and item.entry.tooltipLines or nil

    local selected = item and ns.IsCustomTaskSelected and ns:IsCustomTaskSelected(item.id, characterKey)
    row.check:SetShown(selected == true)
    if row.box.SetBackdropColor then
        if selected then
            row.box:SetBackdropColor(SELECTED[1], SELECTED[2], SELECTED[3], 0.92)
            row.box:SetBackdropBorderColor(GOLD[1], GOLD[2], GOLD[3], 0.88)
        else
            row.box:SetBackdropColor(METAL[1], METAL[2], METAL[3], 0.88)
            row.box:SetBackdropBorderColor(GOLD_DIM[1], GOLD_DIM[2], GOLD_DIM[3], 0.62)
        end
    end
    if row.SetBackdropColor then
        row:SetBackdropColor(METAL_DEEP[1], METAL_DEEP[2], METAL_DEEP[3], selected and 0.66 or 0.48)
        row:SetBackdropBorderColor(GOLD_DIM[1], GOLD_DIM[2], GOLD_DIM[3], 0.34)
    end

    row:SetScript("OnClick", function()
        if not item or not ns.SetCustomTaskSelected then
            return
        end

        ns:SetCustomTaskSelected(item.id, not selected, characterKey, {
            label = item.label,
            groupLabel = item.groupLabel,
        })
    end)
    row:Show()
end

local function ensureMenuRows(card, count, helpers)
    card.menuRows = card.menuRows or {}
    for index = #card.menuRows + 1, count do
        card.menuRows[index] = createMenuRow(card.menuScrollChild, helpers)
    end
end

local function refreshMenu(card, character, snapshot, helpers)
    if not card or not card.menuPanel then
        return
    end

    local menuWidth = math.max(120, math.floor((tonumber(card.menuScroll:GetWidth()) or 320) - 4))
    card.menuScrollChild:SetWidth(menuWidth)

    local catalog = ns.BuildCustomTaskCatalog and ns:BuildCustomTaskCatalog(character, snapshot) or { groups = {} }
    local characterKey = getCharacterKey(character)
    local neededRows = 0
    for _, group in ipairs(catalog.groups or {}) do
        if type(group.items) == "table" and #group.items > 0 then
            neededRows = neededRows + 1 + #group.items
        end
    end
    ensureMenuRows(card, neededRows, helpers)

    local rowIndex = 0
    local y = -4
    for _, group in ipairs(catalog.groups or {}) do
        if type(group.items) == "table" and #group.items > 0 then
            rowIndex = rowIndex + 1
            local header = card.menuRows[rowIndex]
            setMenuRowHeader(header, group.label)
            header:ClearAllPoints()
            header:SetPoint("TOPLEFT", 4, y)
            header:SetPoint("TOPRIGHT", -4, y)
            y = y - 22

            for _, item in ipairs(group.items) do
                rowIndex = rowIndex + 1
                local row = card.menuRows[rowIndex]
                setMenuRowItem(row, item, characterKey)
                row:ClearAllPoints()
                row:SetPoint("TOPLEFT", 4, y)
                row:SetPoint("TOPRIGHT", -4, y)
                y = y - 26
            end
        end
    end

    for index = rowIndex + 1, #card.menuRows do
        card.menuRows[index]:Hide()
    end

    card.menuScrollChild:SetHeight(math.max(10, math.abs(y) + 8))
    if card.menuScroll.UpdateScrollChildRect then
        card.menuScroll:UpdateScrollChildRect()
    end
    card.menuPanel:SetShown(card.menuOpen == true)
end

local function createTaskRow(parent, helpers)
    local createMiniActionButton = callHelper(helpers, "createMiniActionButton")
    local createWeeklyStatusRow = callHelper(helpers, "createWeeklyStatusRow")

    local frame = CreateFrame("Frame", nil, parent)
    frame:SetHeight(34)

    frame.remove = createMiniActionButton(frame, "x", 26)
    frame.remove:SetPoint("RIGHT", 0, 0)
    bindButtonTooltip(frame.remove)

    frame.down = createMiniActionButton(frame, "", 26)
    frame.down:SetPoint("RIGHT", frame.remove, "LEFT", -6, 0)
    setMiniButtonIcon(frame.down, ARROW_DOWN_TEXTURE, 12, 26, 26)
    bindButtonTooltip(frame.down)

    frame.up = createMiniActionButton(frame, "", 26)
    frame.up:SetPoint("RIGHT", frame.down, "LEFT", -6, 0)
    setMiniButtonIcon(frame.up, ARROW_UP_TEXTURE, 12, 26, 26)
    bindButtonTooltip(frame.up)

    frame.statusRow = createWeeklyStatusRow(frame)
    frame.statusRow:SetPoint("LEFT", 0, 0)
    frame.statusRow:SetPoint("RIGHT", frame.up, "LEFT", -8, 0)

    return frame
end

local function ensureTaskRows(card, count, helpers)
    card.taskRows = card.taskRows or {}
    for index = #card.taskRows + 1, count do
        card.taskRows[index] = createTaskRow(card.taskScrollChild, helpers)
    end
end

local function refreshTaskRows(card, view, characterKey, helpers)
    local setWeeklyStatusRow = callHelper(helpers, "setWeeklyStatusRow")
    local rows = type(view) == "table" and view.rows or {}
    ensureTaskRows(card, math.max(1, #rows), helpers)
    local scrollWidth = math.max(220, math.floor((tonumber(card.taskScroll:GetWidth()) or 720) - 4))
    card.taskScrollChild:SetWidth(scrollWidth)

    local y = 0
    if #rows == 0 then
        card.emptyText:SetText(L.CUSTOM_TASKS_EMPTY or "No Fokus rows selected yet. Use the add menu above to choose rows for this character.")
        card.emptyText:Show()
    else
        card.emptyText:Hide()
    end

    for index, rowFrame in ipairs(card.taskRows) do
        local item = rows[index]
        if item then
            rowFrame.taskID = item.id
            rowFrame:ClearAllPoints()
            rowFrame:SetPoint("TOPLEFT", 0, y)
            rowFrame:SetPoint("TOPRIGHT", -4, y)
            setWeeklyStatusRow(rowFrame.statusRow, item.entry, item.label)
            setMiniButtonText(rowFrame.remove, "x", 26, 26)
            setMiniButtonIcon(rowFrame.up, ARROW_UP_TEXTURE, 12, 26, 26)
            setMiniButtonIcon(rowFrame.down, ARROW_DOWN_TEXTURE, 12, 26, 26)
            if rowFrame.up then
                local canMoveUp = index > 1
                rowFrame.up:SetAlpha(canMoveUp and 1 or 0.42)
                rowFrame.up.vlTooltipTitle = L.CUSTOM_TASKS_MOVE_UP or "Move up"
                rowFrame.up.vlTooltipText = L.CUSTOM_TASKS_MOVE_UP_TOOLTIP or "Moves this Fokus row one position up."
                rowFrame.up:SetScript("OnClick", canMoveUp and function()
                    if ns.MoveCustomTask then
                        ns:MoveCustomTask(item.id, -1, characterKey)
                    end
                end or nil)
            end
            if rowFrame.down then
                local canMoveDown = index < #rows
                rowFrame.down:SetAlpha(canMoveDown and 1 or 0.42)
                rowFrame.down.vlTooltipTitle = L.CUSTOM_TASKS_MOVE_DOWN or "Move down"
                rowFrame.down.vlTooltipText = L.CUSTOM_TASKS_MOVE_DOWN_TOOLTIP or "Moves this Fokus row one position down."
                rowFrame.down:SetScript("OnClick", canMoveDown and function()
                    if ns.MoveCustomTask then
                        ns:MoveCustomTask(item.id, 1, characterKey)
                    end
                end or nil)
            end
            rowFrame.remove:SetScript("OnClick", function()
                if ns.SetCustomTaskSelected then
                    ns:SetCustomTaskSelected(item.id, false, characterKey)
                end
            end)
            rowFrame.remove.vlTooltipTitle = L.CUSTOM_TASKS_REMOVE or "Remove"
            rowFrame.remove.vlTooltipText = item.label or ""
            rowFrame:Show()
            y = y - 42
        else
            if rowFrame.up then
                rowFrame.up:SetScript("OnClick", nil)
            end
            if rowFrame.down then
                rowFrame.down:SetScript("OnClick", nil)
            end
            rowFrame:Hide()
        end
    end

    card.taskScrollChild:SetHeight(math.max(80, math.abs(y) + (#rows == 0 and 72 or 8)))
    if card.taskScroll.UpdateScrollChildRect then
        card.taskScroll:UpdateScrollChildRect()
    end
end

local function getTrackerFontLabel(fontKey)
    if fontKey == "compact" then
        return L.CUSTOM_TASKS_TRACKER_FONT_COMPACT or "Compact"
    end
    if fontKey == "large" then
        return L.CUSTOM_TASKS_TRACKER_FONT_LARGE or "Large"
    end

    return L.CUSTOM_TASKS_TRACKER_FONT_NORMAL or "Normal"
end

local function getTrackerStyleLabel(styleKey)
    local styleLabel
    if styleKey == "rows" then
        styleLabel = L.CUSTOM_TASKS_TRACKER_STYLE_ROWS or "Rows"
    elseif styleKey == "text" then
        styleLabel = L.CUSTOM_TASKS_TRACKER_STYLE_TEXT or "Text"
    else
        styleLabel = L.CUSTOM_TASKS_TRACKER_STYLE_FRAME or "Frame"
    end

    return string.format(L.CUSTOM_TASKS_TRACKER_STYLE_FORMAT or "Style: %s", styleLabel)
end

local function getTrackerFonts(fontKey)
    if fontKey == "compact" then
        return "GameFontHighlightSmall", "GameFontNormalSmall"
    end
    if fontKey == "large" then
        return "GameFontHighlight", "GameFontNormal"
    end

    return "GameFontHighlightSmall", "GameFontNormalSmall"
end

local function applyTrackerRowStyle(row, styleKey, fontKey)
    if not row then
        return
    end

    if styleKey == "text" then
        if row.SetBackdropColor then
            row:SetBackdropColor(METAL_DEEP[1], METAL_DEEP[2], METAL_DEEP[3], 0)
            row:SetBackdropBorderColor(GOLD_DIM[1], GOLD_DIM[2], GOLD_DIM[3], 0)
        end
        if row.bg then
            row.bg:Hide()
        end
        if row.fill then
            row.fill:Hide()
        end
        if row.hover then
            row.hover:Hide()
        end
        if row.statusLine then
            row.statusLine:Hide()
        end
        if row.statusBadge then
            row.statusBadge:Hide()
        end
        if row.statusBadgeFrame then
            row.statusBadgeFrame:Hide()
        end
        if row.value then
            row.value:ClearAllPoints()
            row.value:SetPoint("RIGHT", -4, 0)
            row.value:SetWidth(fontKey == "compact" and 70 or 88)
        end
        if row.label then
            row.label:ClearAllPoints()
            row.label:SetPoint("LEFT", 0, 0)
            row.label:SetPoint("RIGHT", row.value, "LEFT", -10, 0)
        end
        return
    end

    if row.SetBackdropColor then
        row:SetBackdropColor(METAL_DEEP[1], METAL_DEEP[2], METAL_DEEP[3], 0.96)
        row:SetBackdropBorderColor(GOLD_DIM[1], GOLD_DIM[2], GOLD_DIM[3], 0.38)
    end
    if row.bg then
        row.bg:Show()
    end
    if row.fill then
        row.fill:Show()
    end
    if row.hover then
        row.hover:Show()
    end
    if row.statusLine then
        row.statusLine:Show()
    end
    if row.value then
        row.value:ClearAllPoints()
        row.value:SetPoint("RIGHT", row.statusBadgeFrame or row.statusBadge, "LEFT", -8, 0)
        row.value:SetWidth(fontKey == "compact" and 70 or 88)
    end
    if row.label then
        row.label:ClearAllPoints()
        row.label:SetPoint("LEFT", 14, 0)
        row.label:SetPoint("RIGHT", row.value, "LEFT", -12, 0)
    end
end

local function refreshControls(card, character, helpers)
    local characterKey = getCharacterKey(character)
    local useGlobal = ns.IsCustomTaskUsingGlobal and ns:IsCustomTaskUsingGlobal(characterKey)
    local settings = ns.GetCustomTaskTrackerSettings and ns:GetCustomTaskTrackerSettings() or {}

    setButtonActive(helpers, card.characterModeButton, not useGlobal)
    setButtonActive(helpers, card.globalModeButton, useGlobal)
    setButtonActive(helpers, card.trackerToggleButton, settings.shown == true)
    setButtonActive(helpers, card.trackerLockButton, settings.locked == true)
    setButtonActive(helpers, card.trackerBackgroundButton, true)

    setMiniButtonText(card.characterModeButton, L.CUSTOM_TASKS_PROFILE_CHARACTER or "This character", 118, 150)
    setMiniButtonText(card.globalModeButton, L.CUSTOM_TASKS_PROFILE_GLOBAL or "Global template", 118, 156)
    setMiniButtonText(card.copyGlobalButton, L.CUSTOM_TASKS_COPY_GLOBAL or "Copy global", 108, 132)
    setMiniButtonText(card.saveGlobalButton, L.CUSTOM_TASKS_SAVE_GLOBAL or "Save as global", 118, 150)
    setMiniButtonText(card.addButton, L.CUSTOM_TASKS_ADD_MENU or "Add focus", 116, 150)
    setMiniButtonText(card.trackerToggleButton, settings.shown and (L.CUSTOM_TASKS_TRACKER_HIDE or "Hide tracker") or (L.CUSTOM_TASKS_TRACKER_SHOW or "Show tracker"), 104, 136)
    setMiniButtonText(card.trackerLockButton, settings.locked and (L.CUSTOM_TASKS_TRACKER_UNLOCK or "Unlock") or (L.CUSTOM_TASKS_TRACKER_LOCK or "Lock"), 70, 92)
    setMiniButtonText(card.trackerBackgroundButton, getTrackerStyleLabel(settings.styleKey), 96, 132)
    setMiniButtonText(card.trackerFontButton, getTrackerFontLabel(settings.fontKey), 78, 104)
    card.scaleValue:SetText(string.format("%d%%", tonumber(settings.scalePercent) or 100))
    card.opacityValue:SetText(string.format("%d%%", tonumber(settings.opacityPercent) or 90))

    card.characterModeButton.vlTooltipTitle = L.CUSTOM_TASKS_PROFILE_CHARACTER or "This character"
    card.characterModeButton.vlTooltipText = L.CUSTOM_TASKS_TOOLTIP_CHARACTER_MODE or "Uses a separate Fokus list for this character."
    card.globalModeButton.vlTooltipTitle = L.CUSTOM_TASKS_PROFILE_GLOBAL or "Global template"
    card.globalModeButton.vlTooltipText = L.CUSTOM_TASKS_TOOLTIP_GLOBAL_MODE or "Uses the shared Fokus list on this character."
    card.copyGlobalButton.vlTooltipTitle = L.CUSTOM_TASKS_COPY_GLOBAL or "Copy global"
    card.copyGlobalButton.vlTooltipText = L.CUSTOM_TASKS_TOOLTIP_COPY_GLOBAL or "Copies the global Fokus list to this character."
    card.saveGlobalButton.vlTooltipTitle = L.CUSTOM_TASKS_SAVE_GLOBAL or "Save as global"
    card.saveGlobalButton.vlTooltipText = L.CUSTOM_TASKS_TOOLTIP_SAVE_GLOBAL or "Saves this character's Fokus list as the global template."
    card.addButton.vlTooltipTitle = L.CUSTOM_TASKS_ADD_MENU or "Add focus"
    card.addButton.vlTooltipText = L.CUSTOM_TASKS_TOOLTIP_ADD_MENU or "Opens the list of trackers and quest rows you can add."
    card.trackerToggleButton.vlTooltipTitle = settings.shown and (L.CUSTOM_TASKS_TRACKER_HIDE or "Hide tracker") or (L.CUSTOM_TASKS_TRACKER_SHOW or "Show tracker")
    card.trackerToggleButton.vlTooltipText = L.CUSTOM_TASKS_TOOLTIP_TRACKER_TOGGLE or "Shows or hides the floating Fokus tracker."
    card.trackerLockButton.vlTooltipTitle = settings.locked and (L.CUSTOM_TASKS_TRACKER_UNLOCK or "Unlock") or (L.CUSTOM_TASKS_TRACKER_LOCK or "Lock")
    card.trackerLockButton.vlTooltipText = L.CUSTOM_TASKS_TOOLTIP_TRACKER_LOCK or "Locks or unlocks dragging for the floating tracker."
    card.trackerBackgroundButton.vlTooltipTitle = getTrackerStyleLabel(settings.styleKey)
    card.trackerBackgroundButton.vlTooltipText = L.CUSTOM_TASKS_TOOLTIP_TRACKER_STYLE or "Cycles the tracker style: frame, rows, or text only."
    card.trackerFontButton.vlTooltipTitle = getTrackerFontLabel(settings.fontKey)
    card.trackerFontButton.vlTooltipText = L.CUSTOM_TASKS_TOOLTIP_TRACKER_FONT or "Cycles the tracker font size."
    card.scaleMinus.vlTooltipTitle = L.CUSTOM_TASKS_TOOLTIP_SCALE_DOWN or "Smaller"
    card.scaleMinus.vlTooltipText = L.CUSTOM_TASKS_TOOLTIP_SCALE_DOWN_TEXT or "Makes the floating tracker smaller."
    card.scalePlus.vlTooltipTitle = L.CUSTOM_TASKS_TOOLTIP_SCALE_UP or "Larger"
    card.scalePlus.vlTooltipText = L.CUSTOM_TASKS_TOOLTIP_SCALE_UP_TEXT or "Makes the floating tracker larger."
    card.opacityMinus.vlTooltipTitle = L.CUSTOM_TASKS_TOOLTIP_OPACITY_DOWN or "Less visible"
    card.opacityMinus.vlTooltipText = L.CUSTOM_TASKS_TOOLTIP_OPACITY_DOWN_TEXT or "Reduces tracker opacity."
    card.opacityPlus.vlTooltipTitle = L.CUSTOM_TASKS_TOOLTIP_OPACITY_UP or "More visible"
    card.opacityPlus.vlTooltipText = L.CUSTOM_TASKS_TOOLTIP_OPACITY_UP_TEXT or "Increases tracker opacity."

    card.characterModeButton:SetScript("OnClick", function()
        if ns.SetCustomTaskUseGlobal then
            ns:SetCustomTaskUseGlobal(characterKey, false)
        end
    end)
    card.globalModeButton:SetScript("OnClick", function()
        if ns.SetCustomTaskUseGlobal then
            ns:SetCustomTaskUseGlobal(characterKey, true)
        end
    end)
    card.copyGlobalButton:SetScript("OnClick", function()
        if ns.CopyGlobalCustomTasksToCharacter then
            ns:CopyGlobalCustomTasksToCharacter(characterKey)
        end
    end)
    card.saveGlobalButton:SetScript("OnClick", function()
        if ns.SaveCharacterCustomTasksAsGlobal then
            ns:SaveCharacterCustomTasksAsGlobal(characterKey)
        end
    end)
    card.addButton:SetScript("OnClick", function()
        card.menuOpen = not card.menuOpen
        card.menuPanel:SetShown(card.menuOpen == true)
    end)
    card.trackerToggleButton:SetScript("OnClick", function()
        if ns.SetCustomTaskTrackerShown then
            ns:SetCustomTaskTrackerShown(not settings.shown)
        end
    end)
    card.trackerLockButton:SetScript("OnClick", function()
        if ns.SetCustomTaskTrackerLocked then
            ns:SetCustomTaskTrackerLocked(not settings.locked)
        end
    end)
    card.trackerBackgroundButton:SetScript("OnClick", function()
        if ns.CycleCustomTaskTrackerStyle then
            ns:CycleCustomTaskTrackerStyle()
        end
    end)
    card.trackerFontButton:SetScript("OnClick", function()
        if ns.CycleCustomTaskTrackerFont then
            ns:CycleCustomTaskTrackerFont()
        end
    end)
    card.scaleMinus:SetScript("OnClick", function()
        if ns.SetCustomTaskTrackerScalePercent then
            ns:SetCustomTaskTrackerScalePercent((tonumber(settings.scalePercent) or 100) - 5)
        end
    end)
    card.scalePlus:SetScript("OnClick", function()
        if ns.SetCustomTaskTrackerScalePercent then
            ns:SetCustomTaskTrackerScalePercent((tonumber(settings.scalePercent) or 100) + 5)
        end
    end)
    card.opacityMinus:SetScript("OnClick", function()
        if ns.SetCustomTaskTrackerOpacityPercent then
            ns:SetCustomTaskTrackerOpacityPercent((tonumber(settings.opacityPercent) or 90) - 5)
        end
    end)
    card.opacityPlus:SetScript("OnClick", function()
        if ns.SetCustomTaskTrackerOpacityPercent then
            ns:SetCustomTaskTrackerOpacityPercent((tonumber(settings.opacityPercent) or 90) + 5)
        end
    end)
end

local function createCard(parent, helpers)
    helpers = helpers or {}
    local createSectionFrame = callHelper(helpers, "createSectionFrame")
    local createLabel = callHelper(helpers, "createLabel")
    local createMiniActionButton = callHelper(helpers, "createMiniActionButton")
    local styleScrollFrame = callHelper(helpers, "styleScrollFrame")

    local card = createSectionFrame(parent, "card")
    card:Hide()
    card.menuOpen = false

    card.summary = createLabel(card, "GameFontHighlightSmall", "LEFT")
    card.summary:SetPoint("TOPLEFT", 14, -16)
    card.summary:SetPoint("TOPRIGHT", -170, -16)

    card.addButton = createMiniActionButton(card, L.CUSTOM_TASKS_ADD_MENU or "Add quest", 124)
    card.addButton:SetPoint("TOPRIGHT", -14, -12)
    bindButtonTooltip(card.addButton)

    card.profileLabel = createLabel(card, "GameFontDisableSmall", "LEFT")
    card.profileLabel:SetPoint("TOPLEFT", 14, -48)
    card.profileLabel:SetWidth(52)
    card.profileLabel:SetText(L.CUSTOM_TASKS_PROFILE_LABEL or "List")

    card.characterModeButton = createMiniActionButton(card, L.CUSTOM_TASKS_PROFILE_CHARACTER or "This character", 126)
    card.characterModeButton:SetPoint("LEFT", card.profileLabel, "RIGHT", 8, 0)
    bindButtonTooltip(card.characterModeButton)
    card.globalModeButton = createMiniActionButton(card, L.CUSTOM_TASKS_PROFILE_GLOBAL or "Global template", 132)
    card.globalModeButton:SetPoint("LEFT", card.characterModeButton, "RIGHT", 8, 0)
    bindButtonTooltip(card.globalModeButton)
    card.copyGlobalButton = createMiniActionButton(card, L.CUSTOM_TASKS_COPY_GLOBAL or "Copy global", 116)
    card.copyGlobalButton:SetPoint("LEFT", card.globalModeButton, "RIGHT", 8, 0)
    bindButtonTooltip(card.copyGlobalButton)
    card.saveGlobalButton = createMiniActionButton(card, L.CUSTOM_TASKS_SAVE_GLOBAL or "Save as global", 126)
    card.saveGlobalButton:SetPoint("LEFT", card.copyGlobalButton, "RIGHT", 8, 0)
    bindButtonTooltip(card.saveGlobalButton)

    card.trackerLabel = createLabel(card, "GameFontDisableSmall", "LEFT")
    card.trackerLabel:SetPoint("TOPLEFT", 14, -80)
    card.trackerLabel:SetWidth(52)
    card.trackerLabel:SetText(L.CUSTOM_TASKS_TRACKER_LABEL or "Tracker")

    card.trackerToggleButton = createMiniActionButton(card, L.CUSTOM_TASKS_TRACKER_SHOW or "Show tracker", 116)
    card.trackerToggleButton:SetPoint("LEFT", card.trackerLabel, "RIGHT", 8, 0)
    bindButtonTooltip(card.trackerToggleButton)
    card.trackerLockButton = createMiniActionButton(card, L.CUSTOM_TASKS_TRACKER_LOCK or "Lock", 76)
    card.trackerLockButton:SetPoint("LEFT", card.trackerToggleButton, "RIGHT", 8, 0)
    bindButtonTooltip(card.trackerLockButton)
    card.trackerBackgroundButton = createMiniActionButton(card, L.CUSTOM_TASKS_TRACKER_STYLE_FORMAT and string.format(L.CUSTOM_TASKS_TRACKER_STYLE_FORMAT, L.CUSTOM_TASKS_TRACKER_STYLE_FRAME or "Frame") or "Style: Frame", 104)
    card.trackerBackgroundButton:SetPoint("LEFT", card.trackerLockButton, "RIGHT", 8, 0)
    bindButtonTooltip(card.trackerBackgroundButton)
    card.trackerFontButton = createMiniActionButton(card, L.CUSTOM_TASKS_TRACKER_FONT_NORMAL or "Normal", 82)
    card.trackerFontButton:SetPoint("LEFT", card.trackerBackgroundButton, "RIGHT", 8, 0)
    bindButtonTooltip(card.trackerFontButton)

    card.scaleMinus = createMiniActionButton(card, "-", 26)
    card.scaleMinus:SetPoint("LEFT", card.trackerFontButton, "RIGHT", 14, 0)
    bindButtonTooltip(card.scaleMinus)
    card.scaleValue = createLabel(card, "GameFontHighlightSmall", "CENTER")
    card.scaleValue:SetPoint("LEFT", card.scaleMinus, "RIGHT", 4, 0)
    card.scaleValue:SetWidth(44)
    card.scalePlus = createMiniActionButton(card, "+", 26)
    card.scalePlus:SetPoint("LEFT", card.scaleValue, "RIGHT", 4, 0)
    bindButtonTooltip(card.scalePlus)

    card.opacityMinus = createMiniActionButton(card, "-", 26)
    card.opacityMinus:SetPoint("LEFT", card.scalePlus, "RIGHT", 14, 0)
    bindButtonTooltip(card.opacityMinus)
    card.opacityValue = createLabel(card, "GameFontHighlightSmall", "CENTER")
    card.opacityValue:SetPoint("LEFT", card.opacityMinus, "RIGHT", 4, 0)
    card.opacityValue:SetWidth(44)
    card.opacityPlus = createMiniActionButton(card, "+", 26)
    card.opacityPlus:SetPoint("LEFT", card.opacityValue, "RIGHT", 4, 0)
    bindButtonTooltip(card.opacityPlus)

    card.taskScroll = CreateFrame("ScrollFrame", nil, card, "UIPanelScrollFrameTemplate")
    card.taskScroll:SetPoint("TOPLEFT", 14, -116)
    card.taskScroll:SetPoint("BOTTOMRIGHT", -28, 18)

    card.taskScrollChild = CreateFrame("Frame", nil, card.taskScroll)
    card.taskScrollChild:SetSize(720, 100)
    card.taskScroll:SetScrollChild(card.taskScrollChild)
    styleScrollFrame(card.taskScroll)

    card.emptyText = createLabel(card.taskScrollChild, "GameFontDisable", "LEFT")
    card.emptyText:SetPoint("TOPLEFT", 4, -4)
    card.emptyText:SetPoint("TOPRIGHT", -4, -4)
    card.emptyText:SetJustifyV("TOP")
    card.emptyText:SetWordWrap(true)

    card.menuPanel = createSectionFrame(card, "cardInset")
    card.menuPanel:SetPoint("TOPRIGHT", card.addButton, "BOTTOMRIGHT", 0, -6)
    card.menuPanel:SetSize(344, 394)
    card.menuPanel:SetFrameLevel(card:GetFrameLevel() + 24)
    card.menuPanel:EnableMouse(true)
    card.menuPanel:Hide()

    card.menuScroll = CreateFrame("ScrollFrame", nil, card.menuPanel, "UIPanelScrollFrameTemplate")
    card.menuScroll:SetPoint("TOPLEFT", 8, -8)
    card.menuScroll:SetPoint("BOTTOMRIGHT", -26, 8)
    card.menuScrollChild = CreateFrame("Frame", nil, card.menuScroll)
    card.menuScrollChild:SetSize(292, 10)
    card.menuScroll:SetScrollChild(card.menuScrollChild)
    styleScrollFrame(card.menuScroll)

    return card
end

local function refreshCard(ui, character, snapshot, helpers)
    if not ui or not ui.frame or not ui.frame.customTaskCard then
        return
    end

    local card = ui.frame.customTaskCard
    character = type(character) == "table" and character or (ns.GetSelectedCharacter and ns:GetSelectedCharacter()) or (ns.GetCurrentCharacterInfo and ns:GetCurrentCharacterInfo())
    snapshot = snapshot or (ns.GetLiveVaultSnapshot and ns:GetLiveVaultSnapshot(character))

    local characterKey = getCharacterKey(character)
    local view = ns.BuildCustomTaskRows and ns:BuildCustomTaskRows(character, snapshot) or { rows = {}, summary = { text = "0/0" } }
    local useGlobal = ns.IsCustomTaskUsingGlobal and ns:IsCustomTaskUsingGlobal(characterKey)
    local profileText = useGlobal and (L.CUSTOM_TASKS_PROFILE_GLOBAL or "Global template") or (L.CUSTOM_TASKS_PROFILE_CHARACTER or "This character")

    card.summary:SetText(string.format(
        L.CUSTOM_TASKS_SUMMARY_FORMAT or "%s selected  |  %s",
        view.summary and view.summary.text or "0/0",
        profileText
    ))

    refreshControls(card, character, helpers)
    refreshTaskRows(card, view, characterKey, helpers)
    refreshMenu(card, character, snapshot, helpers)
end

local function applyTrackerPosition(frame, settings)
    frame:ClearAllPoints()
    local point = type(settings.point) == "table" and settings.point or nil
    if point then
        frame:SetPoint(point.point or "CENTER", UIParent, point.relativePoint or "CENTER", tonumber(point.x) or 360, tonumber(point.y) or 0)
    else
        frame:SetPoint("CENTER", UIParent, "CENTER", 360, 0)
    end
end

local function ensureTracker(ui, helpers)
    if not ui then
        return nil
    end
    if ui.customTaskTracker then
        return ui.customTaskTracker
    end

    helpers = helpers or {}
    local createLabel = callHelper(helpers, "createLabel")

    local frame = CreateFrame("Frame", "VaultloomCustomTaskTrackerFrame", UIParent, BACKDROP_TEMPLATE)
    frame:SetSize(TRACKER_WIDTH, 88)
    frame:SetFrameStrata("MEDIUM")
    frame:SetClampedToScreen(true)
    frame:SetMovable(true)
    frame:EnableMouse(true)
    frame:RegisterForDrag("LeftButton")
    if frame.SetBackdrop then
        frame:SetBackdrop({
            bgFile = "Interface\\Buttons\\WHITE8X8",
            edgeFile = "Interface\\Buttons\\WHITE8X8",
            tile = false,
            edgeSize = 1,
            insets = { left = 1, right = 1, top = 1, bottom = 1 },
        })
    end

    frame.title = createLabel(frame, "GameFontNormalSmall", "LEFT")
    frame.title:SetPoint("TOPLEFT", 10, -8)
    frame.title:SetPoint("TOPRIGHT", -10, -8)
    frame.title:SetText(L.CUSTOM_TASKS_QUESTBOARD_TITLE or "Questboard")
    frame.title:SetTextColor(GOLD[1], GOLD[2], GOLD[3], 1)

    frame.empty = createLabel(frame, "GameFontDisableSmall", "LEFT")
    frame.empty:SetPoint("TOPLEFT", frame.title, "BOTTOMLEFT", 0, -10)
    frame.empty:SetPoint("TOPRIGHT", -10, 0)
    frame.empty:SetWordWrap(true)

    frame.rows = {}
    frame:SetScript("OnDragStart", function(selfFrame)
        local settings = ns.GetCustomTaskTrackerSettings and ns:GetCustomTaskTrackerSettings() or {}
        if settings.locked ~= true then
            selfFrame:StartMoving()
        end
    end)
    frame:SetScript("OnDragStop", function(selfFrame)
        selfFrame:StopMovingOrSizing()
        if ns.SetCustomTaskTrackerPosition then
            local point, _, relativePoint, x, y = selfFrame:GetPoint(1)
            ns:SetCustomTaskTrackerPosition(point, relativePoint, x, y)
        end
    end)

    ui.customTaskTracker = frame
    return frame
end

local function ensureTrackerRows(frame, count, helpers)
    frame.rows = frame.rows or {}
    local createWeeklyStatusRow = callHelper(helpers, "createWeeklyStatusRow")
    for index = #frame.rows + 1, count do
        local row = createWeeklyStatusRow(frame)
        row:SetPoint("TOPLEFT", 10, -30 - ((index - 1) * 38))
        row:SetPoint("TOPRIGHT", -10, -30 - ((index - 1) * 38))
        frame.rows[index] = row
    end
end

local function refreshTracker(ui, helpers)
    local settings = ns.GetCustomTaskTrackerSettings and ns:GetCustomTaskTrackerSettings() or {}

    if settings.shown ~= true then
        if ui and ui.customTaskTracker then
            ui.customTaskTracker:Hide()
        end
        return
    end

    local frame = ensureTracker(ui, helpers)
    if not frame then
        return
    end

    local character = ns.GetCurrentCharacterInfo and ns:GetCurrentCharacterInfo() or nil
    local snapshot = ns.GetLiveVaultSnapshot and ns:GetLiveVaultSnapshot(character) or nil
    local view = ns.BuildCustomTaskRows and ns:BuildCustomTaskRows(character, snapshot) or { rows = {} }
    local rows = view.rows or {}
    local rowCount = math.min(#rows, TRACKER_ROW_LIMIT)

    applyTrackerPosition(frame, settings)
    frame:SetScale((tonumber(settings.scalePercent) or 100) / 100)
    frame:SetAlpha((tonumber(settings.opacityPercent) or 90) / 100)
    frame:EnableMouse(settings.locked ~= true)

    local styleKey = settings.styleKey or (settings.background ~= false and "frame" or "rows")

    if frame.SetBackdropColor then
        if styleKey == "frame" then
            frame:SetBackdropColor(METAL_DEEP[1], METAL_DEEP[2], METAL_DEEP[3], 0.88)
            frame:SetBackdropBorderColor(GOLD_DIM[1], GOLD_DIM[2], GOLD_DIM[3], 0.72)
        elseif styleKey == "text" then
            frame:SetBackdropColor(SHADOW[1], SHADOW[2], SHADOW[3], 0)
            frame:SetBackdropBorderColor(GOLD_DIM[1], GOLD_DIM[2], GOLD_DIM[3], 0)
        else
            frame:SetBackdropColor(SHADOW[1], SHADOW[2], SHADOW[3], 0)
            frame:SetBackdropBorderColor(GOLD_DIM[1], GOLD_DIM[2], GOLD_DIM[3], 0.08)
        end
    end

    ensureTrackerRows(frame, math.max(1, rowCount), helpers)

    local labelFont, valueFont = getTrackerFonts(settings.fontKey)
    local setWeeklyStatusRow = callHelper(helpers, "setWeeklyStatusRow")
    local rowStep = styleKey == "text" and 22 or 38
    local rowTop = styleKey == "text" and -28 or -30

    for index, row in ipairs(frame.rows) do
        local item = rows[index]
        if item and index <= TRACKER_ROW_LIMIT then
            row:ClearAllPoints()
            row:SetHeight(styleKey == "text" and 22 or 34)
            row:SetPoint("TOPLEFT", 10, rowTop - ((index - 1) * rowStep))
            row:SetPoint("TOPRIGHT", -10, rowTop - ((index - 1) * rowStep))
            setWeeklyStatusRow(row, item.entry, item.label)
            if row.label then
                row.label:SetFontObject(labelFont)
            end
            if row.value then
                row.value:SetFontObject(valueFont)
                row.value:SetWidth(settings.fontKey == "compact" and 70 or 88)
            end
            applyTrackerRowStyle(row, styleKey, settings.fontKey)
            row:Show()
        else
            row:Hide()
        end
    end

    if rowCount == 0 then
        frame.empty:SetText(L.CUSTOM_TASKS_TRACKER_EMPTY or "No Fokus rows selected.")
        frame.empty:Show()
    else
        frame.empty:Hide()
    end

    frame:SetHeight(math.max(60, 36 + (rowCount * rowStep) + (rowCount == 0 and 22 or 4)))
    frame:Show()
end

CustomTaskView.CreateCard = createCard
CustomTaskView.RefreshCard = refreshCard
CustomTaskView.RefreshTracker = refreshTracker
CustomTaskView.EnsureTracker = ensureTracker
