
-- Cowmera combat-safe options frame show helper
local CowmeraPendingOptionsFrameShow = false
local CowmeraPendingUIToggle = false

local function CowmeraSafeShowOptionsFrame()
    if InCombatLockdown and InCombatLockdown() then
        CowmeraPendingOptionsFrameShow = true
        print("|cffffcc00Cowmera: options window will open after combat.|r")
        return false
    end

    if CowmeraOptionsFrame then
        CowmeraOptionsFrame:Show()
        return true
    end

    return false
end

local CowmeraOptionsCombatFrame = CreateFrame("Frame")
CowmeraOptionsCombatFrame:RegisterEvent("PLAYER_REGEN_ENABLED")
CowmeraOptionsCombatFrame:SetScript("OnEvent", function()
    if CowmeraPendingOptionsFrameShow then
        CowmeraPendingOptionsFrameShow = false
        CowmeraSafeShowOptionsFrame()
    end
    if CowmeraPendingUIToggle then
        CowmeraPendingUIToggle = false
        if ToggleCowmeraUI then
            ToggleCowmeraUI()
        end
    end
end)

CowmeraDB = CowmeraDB or {}
CowmeraDB.players = CowmeraDB.players or {}

local SLOT_ORDER = {
    { id = 1,  key = "head",      label = "Head" },
    { id = 2,  key = "neck",      label = "Neck" },
    { id = 3,  key = "shoulder",  label = "Shoulder" },
    { id = 5,  key = "chest",     label = "Chest" },
    { id = 6,  key = "waist",     label = "Waist" },
    { id = 7,  key = "legs",      label = "Legs" },
    { id = 8,  key = "feet",      label = "Feet" },
    { id = 9,  key = "wrist",     label = "Wrist" },
    { id = 10, key = "hands",     label = "Hands" },
    { id = 11, key = "finger1",   label = "Finger 1" },
    { id = 12, key = "finger2",   label = "Finger 2" },
    { id = 13, key = "trinket1",  label = "Trinket 1" },
    { id = 14, key = "trinket2",  label = "Trinket 2" },
    { id = 15, key = "back",      label = "Back" },
    { id = 16, key = "mainhand",  label = "Main Hand" },
    { id = 17, key = "offhand",   label = "Off Hand" },
}

local STAT_LABELS = {
    ITEM_MOD_STRENGTH_SHORT = "Strength",
    ITEM_MOD_AGILITY_SHORT = "Agility",
    ITEM_MOD_INTELLECT_SHORT = "Intellect",
    ITEM_MOD_STAMINA_SHORT = "Stamina",
    ITEM_MOD_CRIT_RATING_SHORT = "Critical Strike",
    ITEM_MOD_HASTE_RATING_SHORT = "Haste",
    ITEM_MOD_MASTERY_RATING_SHORT = "Mastery",
    ITEM_MOD_VERSATILITY = "Versatility",
    ITEM_MOD_VERSATILITY_SHORT = "Versatility",
    ITEM_MOD_LEECH_RATING_SHORT = "Leech",
    ITEM_MOD_SPEED_RATING_SHORT = "Speed",
    ITEM_MOD_AVOIDANCE_RATING_SHORT = "Avoidance",
    ITEM_MOD_CR_SPEED_SHORT = "Speed",
    ITEM_MOD_CR_AVOIDANCE_SHORT = "Avoidance",
    ITEM_MOD_CR_LIFESTEAL_SHORT = "Leech",
    ITEM_MOD_CR_STURDINESS_SHORT = "Indestructible",
    ITEM_MOD_DAMAGE_PER_SECOND_SHORT = "WDps",
    RESISTANCE0_NAME = "Armor",
}

local function CowmeraPrint(msg)
    print("|cff22c55e[Cowmera]|r " .. tostring(msg))
end
_G.CowmeraPrint = CowmeraPrint

local function GetPlayerKey()
    local name = UnitName("player") or "Unknown"
    local realm = GetRealmName() or "UnknownRealm"
    return name .. "-" .. realm
end

local function GetStatSortRank(key, label)
    local t = tostring(label or key or ""):lower()
    if t == "armor" then return 1 end
    if t == "stamina" then return 2 end
    if t == "strength" or t == "agility" or t == "intellect" then return 3 end
    if t == "critical strike" or t == "crit" then return 4 end
    if t == "haste" then return 5 end
    if t == "mastery" or t == "master" then return 6 end
    if t == "versatility" then return 7 end
    if t == "wdps" then return 8 end
    if t == "speed" then return 9 end
    if t == "avoidance" then return 10 end
    if t == "leech" then return 11 end
    if t == "indestructible" or t == "indestructable" then return 12 end
    return 99
end

local function NormalizeStats(statsRaw, omitEmptySocket)
    local stats = {}
    if type(statsRaw) ~= "table" then
        return stats
    end

    for statKey, statValue in pairs(statsRaw) do
        if not (omitEmptySocket and tostring(statKey):match("^EMPTY_SOCKET")) then
            table.insert(stats, {
                key = statKey,
                label = STAT_LABELS[statKey] or statKey,
                value = statValue,
            })
        end
    end

    table.sort(stats, function(a, b)
        local ar = GetStatSortRank(a.key, a.label)
        local br = GetStatSortRank(b.key, b.label)
        if ar ~= br then return ar < br end
        return tostring(a.label or "") < tostring(b.label or "")
    end)

    return stats
end

local function DedupLines(lines)
    local out, seen = {}, {}
    for _, line in ipairs(lines or {}) do
        local text = tostring(line or ""):gsub("^%s+", ""):gsub("%s+$", "")
        if text ~= "" and not seen[text] then
            seen[text] = true
            table.insert(out, text)
        end
    end
    return out
end

local function ExpandTooltipTextLines(lines)
    local out = {}
    for _, line in ipairs(lines or {}) do
        local value = tostring(line or "")
        value = value:gsub("|c%x%x%x%x%x%x%x%x", ""):gsub("|r", "")
        value = value:gsub("\r\n", "\n"):gsub("\r", "\n")
        if value:find("\n", 1, true) then
            for part in value:gmatch("([^\n]+)") do
                part = tostring(part or ""):gsub("^%s+", ""):gsub("%s+$", "")
                if part ~= "" then
                    table.insert(out, part)
                end
            end
        else
            value = value:gsub("^%s+", ""):gsub("%s+$", "")
            if value ~= "" then
                table.insert(out, value)
            end
        end
    end
    return out
end

local function ExtractNameFromLink(itemLink)
    if type(itemLink) ~= "string" then
        return ""
    end
    return itemLink:match("|h%[(.-)%]|h") or itemLink:match("%[(.-)%]") or ""
end

local function CopyStatsRaw(statsRaw)
    local out = {}
    if type(statsRaw) ~= "table" then
        return out
    end
    for k, v in pairs(statsRaw) do
        out[k] = tonumber(v) or 0
    end
    return out
end

local function ExtractTextsRecursive(value, out)
    if type(value) == "string" then
        local text = value:gsub("|c%x%x%x%x%x%x%x%x", ""):gsub("|r", ""):gsub("^%s+", ""):gsub("%s+$", "")
        if text ~= "" then
            table.insert(out, text)
        end
        return
    end

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

    if type(value.leftText) == "string" then table.insert(out, value.leftText) end
    if type(value.rightText) == "string" then table.insert(out, value.rightText) end
    if type(value.text) == "string" then table.insert(out, value.text) end
    if type(value.tooltipText) == "string" then table.insert(out, value.tooltipText) end
    if type(value.localizedText) == "string" then table.insert(out, value.localizedText) end

    for _, child in pairs(value) do
        if type(child) == "table" or type(child) == "string" then
            ExtractTextsRecursive(child, out)
        end
    end
end

local function CollectTooltipLines(itemLink, itemName, slotId)
    local lines = {}
    if not itemLink or not C_TooltipInfo then
        return lines
    end

    local tooltipData = nil
    if slotId and C_TooltipInfo.GetInventoryItem then
        local okInv, invData = pcall(C_TooltipInfo.GetInventoryItem, "player", slotId)
        if okInv and type(invData) == "table" then
            tooltipData = invData
        end
    end
    if not tooltipData and C_TooltipInfo.GetHyperlink then
        local ok, linkData = pcall(C_TooltipInfo.GetHyperlink, itemLink)
        if ok and type(linkData) == "table" then
            tooltipData = linkData
        end
    end
    if type(tooltipData) ~= "table" then
        return lines
    end

    local rawTexts = {}
    ExtractTextsRecursive(tooltipData.lines or {}, rawTexts)

    local normalized = {}
    for _, text in ipairs(ExpandTooltipTextLines(rawTexts)) do
        if type(text) == "string" then
            text = text:gsub("|c%x%x%x%x%x%x%x%x", ""):gsub("|r", ""):gsub("^%s+", ""):gsub("%s+$", "")
            if text ~= "" and text ~= tostring(itemName or "") and text ~= ExtractNameFromLink(itemLink) and not text:match("^Item Level ") then
                table.insert(normalized, text)
            end
        end
    end

    return DedupLines(normalized)
end

local function ExtractTooltipText(itemLink, itemName, slotId)
    local tooltipLines = CollectTooltipLines(itemLink, itemName, slotId)
    local flavorText, equipText, useText, effectText = "", "", "", ""
    local misc = {}
    local fullTooltipLines = {}
    local activeBucket = nil
    local setNameLines = {}
    local setBonusLines = {}
    local flavorLines = {}
    local mappedTooltipLines = {}

    local function Append(bucket, line)
        if bucket == "equip" then
            equipText = equipText ~= "" and (equipText .. "\n" .. line) or line
        elseif bucket == "use" then
            useText = useText ~= "" and (useText .. "\n" .. line) or line
        elseif bucket == "effect" then
            effectText = effectText ~= "" and (effectText .. "\n" .. line) or line
        elseif bucket == "flavor" then
            flavorText = flavorText ~= "" and (flavorText .. "\n" .. line) or line
            table.insert(flavorLines, line)
        else
            table.insert(misc, line)
        end
    end

    local function IsBoilerplate(lower, line)
        if lower:match("^sell price") or lower:match("^durability") or lower:match("^classes:") or lower:match("^requires") then return true end
        if lower:match("^socket bonus:") or lower:match("^prismatic socket") or lower:match("^empty socket") then return true end
        if line:find("Socket") and line:find("Bonus") then return true end
        if lower:match("^upgrade level:") or lower:match("^item level ") then return true end
        if lower:match("^%+%d+") and (lower:find("strength") or lower:find("agility") or lower:find("intellect") or lower:find("stamina") or lower:find("critical strike") or lower:find("haste") or lower:find("mastery") or lower:find("versatility") or lower:find("speed") or lower:find("avoidance") or lower:find("leech") or lower:find("indestructible") or lower:find("armor")) then
            return true
        end
        return false
    end

    local function StartsEffect(lower)
        return lower:match("^chance on hit:")
            or lower:match("^proc:")
            or lower:match("^your ")
            or lower:match("^when ")
            or lower:match("^while ")
            or lower:match("^attacks ")
            or lower:match("^spells ")
            or lower:match("^abilities ")
            or lower:match("^dealing ")
            or lower:match("^healing ")
            or lower:match("^taking ")
            or lower:match("^on kill:")
            or lower:match("^on taking damage:")
    end

    local function IsSetBonusStart(lower)
        return lower:match("^set:")
            or lower:match("^%(%d+%)%s*set:")
    end

    local function LooksLikeSetName(line, lower)
        if IsSetBonusStart(lower) then
            return false
        end
        if line:match("%(%d+/%d+%)$") then
            return true
        end
        if lower:find("equipment sets:") == 1 then
            return false
        end
        return false
    end

    for _, line in ipairs(tooltipLines) do
        local clean = tostring(line or ""):gsub("^%s+", ""):gsub("%s+$", "")
        if clean ~= "" then
            table.insert(fullTooltipLines, clean)
            local lower = string.lower(clean)
            local startsNew = false
            local bucketName = "misc"

            if IsSetBonusStart(lower) then
                table.insert(setBonusLines, clean)
                bucketName = "setBonus"
                activeBucket = "setBonus"
                startsNew = true
            elseif LooksLikeSetName(clean, lower) then
                table.insert(setNameLines, clean)
                bucketName = "setName"
                activeBucket = nil
                startsNew = true
            elseif clean:match('^".*"$') then
                activeBucket = "flavor"
                bucketName = "flavor"
                Append(activeBucket, clean)
                startsNew = true
            elseif lower:match("^equip:") then
                activeBucket = "equip"
                bucketName = "equip"
                Append(activeBucket, clean)
                startsNew = true
            elseif lower:match("^use:") then
                activeBucket = "use"
                bucketName = "use"
                Append(activeBucket, clean)
                startsNew = true
            elseif StartsEffect(lower) then
                activeBucket = "effect"
                bucketName = "effect"
                Append(activeBucket, clean)
                startsNew = true
            elseif IsBoilerplate(lower, clean) then
                activeBucket = nil
                bucketName = "boilerplate"
                startsNew = true
            end

            if not startsNew then
                if activeBucket == "setBonus" then
                    if not IsBoilerplate(lower, clean)
                        and not LooksLikeSetName(clean, lower)
                        and not IsSetBonusStart(lower)
                        and lower:find("equipment sets:") ~= 1 then
                        bucketName = "setBonus"
                        table.insert(setBonusLines, clean)
                    else
                        activeBucket = nil
                        bucketName = "misc"
                        table.insert(misc, clean)
                    end
                elseif activeBucket and not lower:match("^unique") and not lower:match("^bind") and not lower:match("^%d+ ") then
                    bucketName = activeBucket
                    Append(activeBucket, clean)
                else
                    activeBucket = nil
                    bucketName = "misc"
                    table.insert(misc, clean)
                end
            end

            table.insert(mappedTooltipLines, {
                text = clean,
                bucket = bucketName,
                isSetBonus = bucketName == "setBonus" and true or false,
                isSetName = bucketName == "setName" and true or false,
                isFlavor = bucketName == "flavor" and true or false,
            })
        end
    end

    return {
        tooltipLines = tooltipLines,
        mappedTooltipLines = mappedTooltipLines,
        flavorLines = flavorLines,
        setNameLines = setNameLines,
        setBonusLines = setBonusLines,
        setBonusText = table.concat(setBonusLines, "\n"),
        flavorText = flavorText,
        equipText = equipText,
        useText = useText,
        effectText = effectText,
        tooltipText = table.concat(misc, "\n"),
        fullTooltipText = table.concat(fullTooltipLines, "\n"),
    }
end

local function CollectSockets(itemLink)
    local sockets = {}
    if not itemLink then return sockets end

    for socketIndex = 1, 4 do
        local gemName, gemLink = nil, nil
        if GetItemGem then
            gemName, gemLink = GetItemGem(itemLink, socketIndex)
        end
        if gemLink and gemLink ~= "" then
            local gemItemID = GetItemInfoInstant and select(1, GetItemInfoInstant(gemLink)) or nil
            local _, _, gemQuality, gemItemLevel = GetItemInfo(gemLink)
            if (not gemItemLevel or gemItemLevel == 0) and C_Item and C_Item.GetDetailedItemLevelInfo then
                gemItemLevel = C_Item.GetDetailedItemLevelInfo(gemLink) or 0
            end
            local gemStatsRaw = {}
            if C_Item and C_Item.GetItemStats then
                gemStatsRaw = C_Item.GetItemStats(gemLink) or {}
            end
            table.insert(sockets, {
                index = socketIndex,
                color = "Prismatic Socket",
                isEmpty = false,
                itemID = gemItemID or 0,
                name = gemName or "Unknown Gem",
                link = gemLink,
                quality = gemQuality or 1,
                itemLevel = gemItemLevel or 0,
                stats = NormalizeStats(gemStatsRaw, true),
            })
        end
    end

    return sockets
end

local function BuildBaseItemStats(statsRaw, sockets)
    local baseStatsRaw = CopyStatsRaw(statsRaw)
    local hasFilledSocket = false

    for _, socket in ipairs(sockets or {}) do
        if type(socket) == "table" and not socket.isEmpty and type(socket.stats) == "table" then
            hasFilledSocket = true
            for _, stat in ipairs(socket.stats) do
                local statKey = tostring(stat.key or "")
                local statValue = tonumber(stat.value) or 0
                if statKey ~= "" and baseStatsRaw[statKey] ~= nil then
                    baseStatsRaw[statKey] = math.max(0, (tonumber(baseStatsRaw[statKey]) or 0) - statValue)
                end
            end
        end
    end

    if hasFilledSocket then
        for statKey, _ in pairs(baseStatsRaw) do
            if tostring(statKey):match("^EMPTY_SOCKET") then
                baseStatsRaw[statKey] = nil
            end
        end
    end

    return baseStatsRaw
end

local function HasEmptySocketStat(statsRaw)
    if type(statsRaw) ~= "table" then return false end
    for statKey, _ in pairs(statsRaw) do
        if tostring(statKey):match("^EMPTY_SOCKET") then
            return true
        end
    end
    return false
end

local function GetClassInfoSafe()
    local localizedClass, classFile = UnitClass("player")
    return localizedClass or "Unknown", classFile or "UNKNOWN"
end

local function GetSpecNameSafe()
    local specName = "Unknown"

    if GetSpecialization and GetSpecializationInfo then
        local specIndex = GetSpecialization()
        if specIndex then
            local _, specLocalized = GetSpecializationInfo(specIndex)
            if specLocalized and specLocalized ~= "" then
                return specLocalized
            end
        end
    end

    if GetInspectSpecialization and GetSpecializationInfoByID then
        local specID = GetInspectSpecialization("player")
        if specID and specID > 0 then
            local _, specLocalized = GetSpecializationInfoByID(specID)
            if specLocalized and specLocalized ~= "" then
                return specLocalized
            end
        end
    end

    return specName
end

local function BuildPlayerRecord()
    local playerKey = GetPlayerKey()
    local existing = CowmeraDB.players[playerKey] or {}
    local playerRecord = existing

    local playerName = UnitName("player") or "Unknown"
    local realmName = GetRealmName() or "UnknownRealm"
    local localizedClass, classFile = GetClassInfoSafe()
    local specName = GetSpecNameSafe()

    playerRecord.characterName = playerName
    playerRecord.realmName = realmName
    playerRecord.fullName = playerName .. "-" .. realmName
    playerRecord.classLocalized = localizedClass
    playerRecord.classFile = classFile
    playerRecord.spec = specName
    playerRecord.name = playerName
    playerRecord.realm = realmName
    playerRecord.class = classFile
    playerRecord.level = UnitLevel("player") or 0
    playerRecord.updatedAt = time()
    playerRecord.dataVersion = 4
    playerRecord.gear = {}

    local totalIlvl, totalCount, resolvedCount, unresolvedCount = 0, 0, 0, 0

    for _, slot in ipairs(SLOT_ORDER) do
        local itemLink = GetInventoryItemLink("player", slot.id)
        local itemID = GetInventoryItemID("player", slot.id)

        if itemLink and itemID then
            local linkName = ExtractNameFromLink(itemLink)
            local itemName, _, itemQuality, _, _, itemType, itemSubType, _, itemEquipLoc = GetItemInfo(itemLink)
            if (not itemName or itemName == "" or itemName == UNKNOWN or itemName == "Unknown") and C_Item and C_Item.GetItemNameByID then
                itemName = C_Item.GetItemNameByID(itemID)
            end
            if not itemName or itemName == "" or itemName == UNKNOWN or itemName == "Unknown" then
                itemName = linkName
            end

            local itemLevel = 0
            if C_Item and C_Item.GetDetailedItemLevelInfo then
                itemLevel = C_Item.GetDetailedItemLevelInfo(itemLink) or 0
            end

            local statsRaw = {}
            if C_Item and C_Item.GetItemStats then
                statsRaw = C_Item.GetItemStats(itemLink) or {}
            end

            local sockets = CollectSockets(itemLink)
            local baseStatsRaw = BuildBaseItemStats(statsRaw, sockets)
            local stats = NormalizeStats(baseStatsRaw, false)
            local tooltipText = ExtractTooltipText(itemLink, itemName, slot.id)
            local unresolved = (not itemName or itemName == "" or itemName == UNKNOWN or itemName == "Unknown" or itemLevel <= 0)

            playerRecord.gear[slot.key] = {
                slotId = slot.id,
                slotKey = slot.key,
                slotName = slot.label,
                itemID = itemID,
                link = itemLink,
                name = itemName or linkName or "Unknown",
                quality = itemQuality or 1,
                itemLevel = itemLevel,
                itemType = itemType or "",
                subType = itemSubType or "",
                inventoryType = itemEquipLoc or "",
                sockets = sockets,
                stats = stats,
                unresolved = unresolved or nil,
                description = "",
                flavorText = tooltipText.flavorText or "",
                equipText = tooltipText.equipText or "",
                useText = tooltipText.useText or "",
                effectText = tooltipText.effectText or "",
                tooltipText = tooltipText.tooltipText or "",
                fullTooltipText = tooltipText.fullTooltipText or "",
                tooltipLines = tooltipText.tooltipLines or {},
                mappedTooltipLines = tooltipText.mappedTooltipLines or {},
                flavorLines = tooltipText.flavorLines or {},
                setNameLines = tooltipText.setNameLines or {},
                setBonusLines = tooltipText.setBonusLines or {},
                setBonusText = tooltipText.setBonusText or "",
                socketBonusText = HasEmptySocketStat(baseStatsRaw) and "Has socket bonus or socketable slot" or "",
            }

            if unresolved then
                unresolvedCount = unresolvedCount + 1
            else
                resolvedCount = resolvedCount + 1
            end

            if itemLevel > 0 then
                totalIlvl = totalIlvl + itemLevel
                totalCount = totalCount + 1
            end
        else
            playerRecord.gear[slot.key] = {
                slotId = slot.id,
                slotKey = slot.key,
                slotName = slot.label,
                empty = true,
            }
        end
    end

    if totalCount > 0 then
        playerRecord.averageItemLevel = math.floor((totalIlvl / totalCount) * 100 + 0.5) / 100
    else
        playerRecord.averageItemLevel = 0
    end

    playerRecord.resolvedSlotCount = resolvedCount
    playerRecord.unresolvedSlotCount = unresolvedCount
    playerRecord.snapshotComplete = (unresolvedCount == 0)

    return playerRecord
end

local function SaveSnapshot()
    local playerKey = GetPlayerKey()
    CowmeraDB.players[playerKey] = BuildPlayerRecord()
    CowmeraDB.lastUpdated = time()
    return CowmeraDB.players[playerKey]
end

local function ReadGear()
    return BuildPlayerRecord()
end

local function PrintGear()
    local playerRecord = ReadGear()
    CowmeraPrint("Gear for " .. (playerRecord.fullName or playerRecord.name or "Unknown") .. " |cffaaaaaa(" .. tostring(playerRecord.classLocalized or playerRecord.class or "Unknown") .. " / " .. tostring(playerRecord.spec or "Unknown") .. ", avg ilvl " .. tostring(playerRecord.averageItemLevel) .. ")|r")
    if playerRecord.unresolvedSlotCount and playerRecord.unresolvedSlotCount > 0 then
        CowmeraPrint("Warning: " .. tostring(playerRecord.unresolvedSlotCount) .. " slot(s) still have unresolved item data in the live preview.")
    end
    for _, slot in ipairs(SLOT_ORDER) do
        local item = playerRecord.gear[slot.key]
        local slotNumberColored = "|cffff9900[" .. tostring(slot.id) .. "]|r"
        if item and not item.empty then
            local itemNameColored = item.link or item.name or "Unknown"
            CowmeraPrint(slotNumberColored .. " " .. slot.label .. ": " .. itemNameColored .. " |cffaaaaaa(ilvl " .. tostring(item.itemLevel) .. ")|r")
        else
            CowmeraPrint(slotNumberColored .. " " .. slot.label .. ": |cff9d9d9dEmpty|r")
        end
    end
end

local function PrintSlot(slotKey)
    local playerRecord = ReadGear()
    local item = playerRecord.gear[slotKey]
    if not item or item.empty then
        CowmeraPrint("That slot is empty.")
        return
    end

    local slotNumberColored = "|cffff9900[" .. tostring(item.slotId) .. "]|r"
    local itemNameColored = item.link or item.name or "Unknown"
    CowmeraPrint(slotNumberColored .. " " .. item.slotName .. ": " .. itemNameColored)
    CowmeraPrint("|cffaaaaaaItem Level:|r " .. tostring(item.itemLevel))
    if item.stats and #item.stats > 0 then
        for _, stat in ipairs(item.stats) do
            CowmeraPrint("|cffaaaaaa" .. tostring(stat.label) .. ":|r " .. tostring(stat.value))
        end
    else
        CowmeraPrint("|cff9d9d9dNo stats found.|r")
    end
end



local function EnsureOptions()
    CowmeraDB.options = CowmeraDB.options or {}
    local o = CowmeraDB.options
    if o.autoSnapshotOnLogin == nil then o.autoSnapshotOnLogin = true end
    if o.autoSnapshotOnEnterWorld == nil then o.autoSnapshotOnEnterWorld = true end
    if o.autoSnapshotOnEquipment == nil then o.autoSnapshotOnEquipment = true end
    if o.autoSnapshotOnBag == nil then o.autoSnapshotOnBag = false end
    if o.autoSnapshotOnSpec == nil then o.autoSnapshotOnSpec = false end
    if o.autoSnapshotOnAverageItemLevel == nil then o.autoSnapshotOnAverageItemLevel = false end
    return o
end

local CowmeraUI = nil

_G.Cowmera = _G.Cowmera or {}
_G.Cowmera.OpenUI = function()
    if ToggleCowmeraUI then ToggleCowmeraUI() end
end

local function CreateCheckButton(parent, labelText, tooltipText, onClick)
    local cb = CreateFrame("CheckButton", nil, parent, "UICheckButtonTemplate")
    cb.Text:SetText(labelText or "")
    cb.Text:SetTextColor(1, 0.82, 0, 1)
    cb:SetScript("OnClick", function(self)
        if onClick then onClick(self:GetChecked() and true or false) end
    end)
    if tooltipText and tooltipText ~= "" then
        cb:SetScript("OnEnter", function(self)
            GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
            GameTooltip:SetText(labelText or "Cowmera")
            GameTooltip:AddLine(tooltipText, 1, 1, 1, true)
            GameTooltip:Show()
        end)
        cb:SetScript("OnLeave", function() GameTooltip:Hide() end)
    end
    return cb
end

local function CreateActionButton(parent, width, height, text, x, y, onClick)
    local btn = CreateFrame("Button", nil, parent, "UIPanelButtonTemplate")
    btn:SetSize(width, height)
    btn:SetPoint("TOPLEFT", parent, "TOPLEFT", x, y)
    btn:SetText(text or "Button")
    btn:SetScript("OnClick", function()
        if onClick then onClick() end
    end)
    return btn
end



local CowmeraMinimapButton = nil
local ToggleCowmeraUI

local function EnsureButtonSettings()
    CowmeraDB.ui = CowmeraDB.ui or {}
    local ui = CowmeraDB.ui
    if ui.showMinimapButton == nil then ui.showMinimapButton = true end
    if type(ui.minimapButton) ~= "table" then
        ui.minimapButton = { angle = 220, radius = 86 }
    end
    return ui
end

local function UpdateMinimapButtonPosition(btn)
    if not btn or not Minimap then return end
    local ui = EnsureButtonSettings()
    local data = ui.minimapButton or {}
    local angle = math.rad(tonumber(data.angle) or 220)
    local radius = tonumber(data.radius) or 80
    local x = math.cos(angle) * radius
    local y = math.sin(angle) * radius
    btn:ClearAllPoints()
    btn:SetPoint("CENTER", Minimap, "CENTER", x, y)
end

local function SaveMinimapButtonPositionFromCursor(btn)
    if not btn or not Minimap then return end
    local mx, my = Minimap:GetCenter()
    if not mx or not my then return end
    local scale = Minimap:GetEffectiveScale() or 1
    local cx, cy = GetCursorPosition()
    cx = cx / scale
    cy = cy / scale
    local dx = cx - mx
    local dy = cy - my
    local angle = math.deg(math.atan2(dy, dx))
    local radius = math.sqrt((dx * dx) + (dy * dy))
    local ui = EnsureButtonSettings()
    ui.minimapButton = {
        angle = angle,
        radius = math.max(80, math.min(110, radius)),
    }
    UpdateMinimapButtonPosition(btn)
end

local function UpdateMinimapButtonState()
    if not CowmeraMinimapButton then return end
    local ui = EnsureButtonSettings()
    if ui.showMinimapButton then
        CowmeraMinimapButton:Show()
    else
        CowmeraMinimapButton:Hide()
    end
end

local function EnsureMinimapButton()
    local ui = EnsureButtonSettings()
    if CowmeraMinimapButton then
        UpdateMinimapButtonState()
        UpdateMinimapButtonPosition(CowmeraMinimapButton)
        return CowmeraMinimapButton
    end

    if not Minimap then return nil end

    local btn = CreateFrame("Button", "CowmeraMinimapToggleButton", Minimap)
    btn:SetSize(36, 36)
    btn:SetFrameStrata("MEDIUM")
    btn:SetFrameLevel((Minimap:GetFrameLevel() or 1) + 8)
    btn:EnableMouse(true)
    btn:RegisterForClicks("LeftButtonUp", "RightButtonUp")
    btn:RegisterForDrag("LeftButton")
    btn:SetMovable(true)
    btn:SetClampedToScreen(true)

    local background = btn:CreateTexture(nil, "BACKGROUND")
    background:SetSize(22, 22)
    background:SetPoint("CENTER", 0, 1)
    background:SetTexture("Interface\\Minimap\\MiniMap-TrackingBackground")
    btn.background = background

    local icon = btn:CreateTexture(nil, "ARTWORK")
    icon:SetSize(20, 20)
    icon:SetPoint("CENTER", 0, 1)
    icon:SetTexture("Interface\\AddOns\\Cowmera\\media\\cowang.tga")
    btn.icon = icon

    local overlay = btn:CreateTexture(nil, "OVERLAY")
    overlay:SetSize(54, 54)
    overlay:SetPoint("TOPLEFT")
    overlay:SetTexture("Interface\\Minimap\\MiniMap-TrackingBorder")
    btn.overlay = overlay

    local highlight = btn:CreateTexture(nil, "HIGHLIGHT")
    highlight:SetTexture("Interface\\Minimap\\UI-Minimap-ZoomButton-Highlight")
    highlight:SetBlendMode("ADD")
    highlight:SetAllPoints(btn)

    btn:SetScript("OnEnter", function(self)
        GameTooltip:SetOwner(self, "ANCHOR_LEFT")
        GameTooltip:SetText("Cowmera")
        GameTooltip:AddLine("Left-click: open or close the Cowmera window", 1, 1, 1, true)
        GameTooltip:AddLine("Drag: move this minimap button", 0.8, 0.8, 0.8, true)
        GameTooltip:AddLine("Right-click: reset minimap button position", 0.8, 0.8, 0.8, true)
        GameTooltip:Show()
    end)
    btn:SetScript("OnLeave", function() GameTooltip:Hide() end)

    btn:SetScript("OnDragStart", function(self)
        self:SetScript("OnUpdate", function(b)
            SaveMinimapButtonPositionFromCursor(b)
        end)
    end)
    btn:SetScript("OnDragStop", function(self)
        self:SetScript("OnUpdate", nil)
        SaveMinimapButtonPositionFromCursor(self)
    end)

    btn:SetScript("OnClick", function(self, button)
        if button == "RightButton" then
            local uiData = EnsureButtonSettings()
            uiData.showMinimapButton = true
            uiData.minimapButton = { angle = 220, radius = 86 }
            UpdateMinimapButtonState()
            UpdateMinimapButtonPosition(self)
            CowmeraPrint("Minimap button position reset.")
            return
        end
        if CowmeraUI and CowmeraUI:IsShown() then
            CowmeraUI:Hide()
        else
            ToggleCowmeraUI()
        end
    end)

    CowmeraMinimapButton = btn
    btn:Show()
    UpdateMinimapButtonPosition(btn)
    UpdateMinimapButtonState()
    return btn
end


local function JsonEscapeString(value)
    local s = tostring(value or "")
    s = s:gsub('\\', '\\\\')
    s = s:gsub('"', '\\"')
    s = s:gsub('\n', '\\n')
    s = s:gsub('\r', '\\r')
    s = s:gsub('\t', '\\t')
    return '"' .. s .. '"'
end

local function IsArrayTable(t)
    if type(t) ~= "table" then return false end
    local maxIndex = 0
    local count = 0
    for k, _ in pairs(t) do
        if type(k) ~= "number" or k < 1 or math.floor(k) ~= k then
            return false
        end
        if k > maxIndex then maxIndex = k end
        count = count + 1
    end
    return maxIndex == count
end

local function SerializeJson(value, indent)
    indent = indent or 0
    local pad = string.rep("  ", indent)
    local nextPad = string.rep("  ", indent + 1)
    local valueType = type(value)

    if valueType == "nil" then
        return "null"
    elseif valueType == "boolean" then
        return value and "true" or "false"
    elseif valueType == "number" then
        return tostring(value)
    elseif valueType == "string" then
        return JsonEscapeString(value)
    elseif valueType ~= "table" then
        return JsonEscapeString(tostring(value))
    end

    if next(value) == nil then
        return IsArrayTable(value) and "[]" or "{}"
    end

    local parts = {}
    if IsArrayTable(value) then
        for i = 1, #value do
            table.insert(parts, nextPad .. SerializeJson(value[i], indent + 1))
        end
        return "[\n" .. table.concat(parts, ",\n") .. "\n" .. pad .. "]"
    end

    local keys = {}
    for k, _ in pairs(value) do
        table.insert(keys, k)
    end
    table.sort(keys, function(a, b) return tostring(a) < tostring(b) end)
    for _, k in ipairs(keys) do
        table.insert(parts, nextPad .. JsonEscapeString(k) .. ": " .. SerializeJson(value[k], indent + 1))
    end
    return "{\n" .. table.concat(parts, ",\n") .. "\n" .. pad .. "}"
end

local function GetSavedPlayerRecord()
    return (CowmeraDB.players or {})[GetPlayerKey()]
end

local function BuildSavedGearSummary(record)
    if type(record) ~= "table" then
        return "No saved snapshot yet. Click Save Snapshot or Reload / Update."
    end

    local total, filled = 0, 0
    if type(record.gear) == "table" then
        for _, slot in ipairs(SLOT_ORDER) do
            total = total + 1
            local item = record.gear[slot.key]
            if item and not item.empty and item.name then
                filled = filled + 1
            end
        end
    end

    local updatedAt = tonumber(record.updatedAt)
    local updatedText = updatedAt and date("%Y-%m-%d %H:%M:%S", updatedAt) or "Unknown"
    return string.format(
        "%s | %s %s | Level %s | Avg ilvl %s | %d/%d slots saved | Updated %s",
        tostring(record.fullName or record.name or "Unknown"),
        tostring(record.classLocalized or record.class or "Unknown"),
        tostring(record.spec or "Unknown"),
        tostring(record.level or "?"),
        tostring(record.averageItemLevel or 0),
        filled,
        total,
        updatedText
    )
end

local function BuildSavedOutputText(record)
    if type(record) ~= "table" then
        return "No saved data yet."
    end
    return SerializeJson(record, 0)
end

local function CowmeraSafeCall(func, ...)
    if type(func) ~= "function" then return false end
    local ok = pcall(func, ...)
    return ok
end

local function CowmeraTryLoadBlizzardUI(...)
    for i = 1, select("#", ...) do
        local addonName = select(i, ...)
        if type(addonName) == "string" and addonName ~= "" then
            if C_AddOns and type(C_AddOns.IsAddOnLoaded) == "function" and C_AddOns.IsAddOnLoaded(addonName) then
                return true
            end
            if type(UIParentLoadAddOn) == "function" then
                local ok = pcall(UIParentLoadAddOn, addonName)
                if ok then
                    if not C_AddOns or type(C_AddOns.IsAddOnLoaded) ~= "function" or C_AddOns.IsAddOnLoaded(addonName) then
                        return true
                    end
                end
            end
            if type(LoadAddOn) == "function" then
                local ok = pcall(LoadAddOn, addonName)
                if ok then
                    if not C_AddOns or type(C_AddOns.IsAddOnLoaded) ~= "function" or C_AddOns.IsAddOnLoaded(addonName) then
                        return true
                    end
                end
            end
        end
    end
    return false
end

local function CowmeraTryOpenHousing()
    if _G.HousingMicroButton and _G.HousingMicroButton.Click then
        if CowmeraSafeCall(_G.HousingMicroButton.Click, _G.HousingMicroButton, "LeftButton") then
            return true
        end
        if CowmeraSafeCall(_G.HousingMicroButton.Click, _G.HousingMicroButton) then
            return true
        end
    end
    CowmeraPrint("Could not open Housing.")
    return false
end

local function CowmeraTryOpenAppearance()
    if type(_G.ToggleCollectionsJournal) == "function" then
        local ok = pcall(_G.ToggleCollectionsJournal, 5)
        if ok then return true end
        if CowmeraSafeCall(_G.ToggleCollectionsJournal) then return true end
    end
    if _G.CollectionsJournal then
        ShowUIPanel(_G.CollectionsJournal)
        return true
    end
    CowmeraPrint("Could not open the Appearance tab.")
    return false
end

local function CowmeraTryOpenGroupFinder()
    if type(_G.PVEFrame_ToggleFrame) == "function" then
        local ok = pcall(_G.PVEFrame_ToggleFrame, "GroupFinderFrame")
        if ok then return true end
    end
    if type(_G.ToggleLFDParentFrame) == "function" and CowmeraSafeCall(_G.ToggleLFDParentFrame) then
        return true
    end
    CowmeraPrint("Could not open Group Finder.")
    return false
end

local function CowmeraTryOpenProfessions()
    if type(_G.ToggleProfessionsBook) == "function" and CowmeraSafeCall(_G.ToggleProfessionsBook) then
        return true
    end
    if type(_G.CastSpellByName) == "function" and CowmeraSafeCall(_G.CastSpellByName, TOGGLE_PROFESSIONBOOK or "Professions") then
        return true
    end
    CowmeraPrint("Could not open Professions.")
    return false
end

local function CowmeraTryOpenTalents()
    CowmeraTryLoadBlizzardUI("Blizzard_PlayerSpells")

    if _G.PlayerSpellsUtil and type(_G.PlayerSpellsUtil.TogglePlayerSpellsFrame) == "function" then
        if CowmeraSafeCall(_G.PlayerSpellsUtil.TogglePlayerSpellsFrame, 3) then
            local maximizeButton = _G.PlayerSpellsFrame and _G.PlayerSpellsFrame.MaximizeMinimizeButton and _G.PlayerSpellsFrame.MaximizeMinimizeButton.MaximizeButton
            if maximizeButton and maximizeButton.IsShown and maximizeButton:IsShown() and maximizeButton.Click then
                CowmeraSafeCall(maximizeButton.Click, maximizeButton)
            end
            return true
        end
    end
    if type(_G.ToggleTalentFrame) == "function" and CowmeraSafeCall(_G.ToggleTalentFrame) then
        return true
    end
    if _G.ClassTalentFrame then
        ShowUIPanel(_G.ClassTalentFrame)
        return true
    end
    if _G.PlayerSpellsFrame then
        ShowUIPanel(_G.PlayerSpellsFrame)
        return true
    end
    CowmeraPrint("Could not open Talents & Spellbook.")
    return false
end

local function CowmeraTryOpenAdventureGuide()
    if type(_G.ToggleEncounterJournal) == "function" and CowmeraSafeCall(_G.ToggleEncounterJournal) then
        return true
    end
    if _G.EncounterJournal then
        ShowUIPanel(_G.EncounterJournal)
        return true
    end
    CowmeraPrint("Could not open the Adventure Guide.")
    return false
end

local function CowmeraTryOpenDungeons()
    if type(_G.PVEFrame_ToggleFrame) == "function" then
        local ok = pcall(_G.PVEFrame_ToggleFrame, "ChallengesFrame")
        if ok then return true end
    end
    if type(_G.ToggleEncounterJournal) == "function" and CowmeraSafeCall(_G.ToggleEncounterJournal) then
        return true
    end
    CowmeraPrint("Could not open Dungeons.")
    return false
end

local function CowmeraSelectRaidTab()
    if _G.EncounterJournalNavBarHomeButton and _G.EncounterJournalNavBarHomeButton.Click then
        CowmeraSafeCall(_G.EncounterJournalNavBarHomeButton.Click, _G.EncounterJournalNavBarHomeButton)
    end

    local candidates = {
        _G.EncounterJournal and _G.EncounterJournal.instanceSelect and _G.EncounterJournal.instanceSelect.raidTab or nil,
        _G.EncounterJournal and _G.EncounterJournal.instanceSelect and _G.EncounterJournal.instanceSelect.RaidTab or nil,
        _G.EncounterJournalInstanceSelect and _G.EncounterJournalInstanceSelect.raidTab or nil,
        _G.EncounterJournalInstanceSelect and _G.EncounterJournalInstanceSelect.RaidTab or nil,
    }

    for _, tab in ipairs(candidates) do
        if tab and tab.Click and CowmeraSafeCall(tab.Click, tab) then
            return true
        end
    end

    local parents = {
        _G.EncounterJournal and _G.EncounterJournal.instanceSelect or nil,
        _G.EncounterJournalInstanceSelect,
    }

    for _, parent in ipairs(parents) do
        if parent and parent.GetChildren then
            local children = { parent:GetChildren() }
            for _, child in ipairs(children) do
                if child and child.Click then
                    local textObj = child.GetText and child:GetText() or nil
                    local label = textObj or (child.Text and child.Text.GetText and child.Text:GetText()) or nil
                    label = tostring(label or ""):lower()
                    if label:find("raid") then
                        if CowmeraSafeCall(child.Click, child) then
                            return true
                        end
                    end
                end
            end
        end
    end

    return false
end

local function CowmeraTryOpenCharacter()
    if _G.CharacterMicroButton and _G.CharacterMicroButton.Click then
        if CowmeraSafeCall(_G.CharacterMicroButton.Click, _G.CharacterMicroButton, "LeftButton") then
            return true
        end
        if CowmeraSafeCall(_G.CharacterMicroButton.Click, _G.CharacterMicroButton) then
            return true
        end
    end
    if type(_G.ToggleCharacter) == "function" and CowmeraSafeCall(_G.ToggleCharacter, "PaperDollFrame") then
        return true
    end
    if _G.PaperDollFrame then
        ShowUIPanel(_G.PaperDollFrame)
        return true
    end
    CowmeraPrint("Could not open Character.")
    return false
end

local function CowmeraTryOpenGuild()
    if type(_G.ToggleGuildFrame) == "function" and CowmeraSafeCall(_G.ToggleGuildFrame) then
        return true
    end
    if _G.GuildFrame then
        ShowUIPanel(_G.GuildFrame)
        return true
    end
    CowmeraPrint("Could not open Guild.")
    return false
end

local COWMERA_NAVIGATION_BUTTONS = {
    { text = "Housing", onClick = CowmeraTryOpenHousing },
    { text = "Appearance", onClick = CowmeraTryOpenAppearance },
    { text = "Group Finder", onClick = CowmeraTryOpenGroupFinder },
    { text = "Professions", onClick = CowmeraTryOpenProfessions },
    { text = "Talents", onClick = CowmeraTryOpenTalents },
    { text = "Adventure Guide", onClick = CowmeraTryOpenAdventureGuide },
    { text = "Dungeons", onClick = CowmeraTryOpenDungeons },
    { text = "Character", onClick = CowmeraTryOpenCharacter },
    { text = "Guild", onClick = CowmeraTryOpenGuild },
}

ToggleCowmeraUI = function()
    if InCombatLockdown and InCombatLockdown() then
        CowmeraPendingUIToggle = true
        print("|cffffcc00Cowmera: main window will open after combat.|r")
        return
    end

    local opts = EnsureOptions()
    if not CowmeraUI then
        local f = CreateFrame("Frame", "CowmeraOptionsFrame", UIParent, "BackdropTemplate")
        f:SetSize(920, 680)
        f:SetPoint("CENTER", UIParent, "CENTER", 0, -45)
        f:SetMovable(true)
        f:EnableMouse(true)
        f:RegisterForDrag("LeftButton")
        f:SetScript("OnDragStart", f.StartMoving)
        f:SetScript("OnDragStop", f.StopMovingOrSizing)
        f:SetFrameStrata("HIGH")
        f:SetFrameLevel(200)
        if f.SetBackdrop then
            f:SetBackdrop({
                bgFile = "Interface\\AddOns\\Cowmera\\media\\cowang.tga",
                edgeFile = "Interface/Tooltips/UI-Tooltip-Border",
                tile = false,
                edgeSize = 16,
                insets = { left = 4, right = 4, top = 4, bottom = 4 },
            })
            f:SetBackdropColor(1, 1, 1, 0.72)
            f:SetBackdropBorderColor(0.8, 0.45, 0.1, 1)
        end

        local close = CreateFrame("Button", nil, f, "UIPanelCloseButton")
        close:SetPoint("TOPRIGHT", -4, -4)

        table.insert(UISpecialFrames, "CowmeraOptionsFrame")

        local sidebar = CreateFrame("Frame", nil, f, "BackdropTemplate")
        sidebar:SetPoint("TOPLEFT", 12, -14)
        sidebar:SetPoint("BOTTOMLEFT", 12, 14)
        sidebar:SetWidth(160)
        if sidebar.SetBackdrop then
            sidebar:SetBackdrop({
                bgFile = "Interface/ChatFrame/ChatFrameBackground",
                edgeFile = "Interface/Tooltips/UI-Tooltip-Border",
                tile = true,
                tileSize = 16,
                edgeSize = 12,
                insets = { left = 3, right = 3, top = 3, bottom = 3 },
            })
            sidebar:SetBackdropColor(0, 0, 0, 0.45)
            sidebar:SetBackdropBorderColor(0.65, 0.4, 0.08, 0.9)
        end

        local sidebarTitle = sidebar:CreateFontString(nil, "ARTWORK", "GameFontNormalLarge")
        sidebarTitle:SetPoint("TOP", 0, -18)
        sidebarTitle:SetText("Cowmera")

        local sidebarNote = sidebar:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall")
        sidebarNote:SetPoint("TOP", sidebarTitle, "BOTTOM", 0, -6)
        sidebarNote:SetWidth(124)
        sidebarNote:SetJustifyH("CENTER")
        sidebarNote:SetText("")

        local content = CreateFrame("Frame", nil, f)
        content:SetPoint("TOPLEFT", sidebar, "TOPRIGHT", 14, 0)
        content:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -14, 14)

        local navButtons = {}
        local function SetNavActive(activeKey)
            for key, btn in pairs(navButtons) do
                if key == activeKey then
                    btn:LockHighlight()
                else
                    btn:UnlockHighlight()
                end
            end
        end

        local function CreateNavButton(parent, textValue, key, x, y)
            local btn = CreateFrame("Button", nil, parent, "UIPanelButtonTemplate")
            btn:SetSize(124, 28)
            btn:SetPoint("TOPLEFT", x, y)
            btn:SetText(textValue or "Tab")
            navButtons[key] = btn
            return btn
        end

        local teleportsPanel = CreateFrame("Frame", nil, content)
        teleportsPanel:SetAllPoints()
        teleportsPanel:SetFrameStrata(f:GetFrameStrata())
        teleportsPanel:SetFrameLevel(content:GetFrameLevel() + 1)
        teleportsPanel:Hide()

        local toolsPanel = CreateFrame("Frame", nil, content)
        toolsPanel:SetAllPoints()
        toolsPanel:SetFrameStrata(f:GetFrameStrata())
        toolsPanel:SetFrameLevel(content:GetFrameLevel() + 1)
        toolsPanel:Hide()

        local navigationPanel = CreateFrame("Frame", nil, content)
        navigationPanel:SetAllPoints()
        navigationPanel:SetFrameStrata(f:GetFrameStrata())
        navigationPanel:SetFrameLevel(content:GetFrameLevel() + 1)
        navigationPanel:Hide()

        local keysPanel = CreateFrame("Frame", nil, content)
        keysPanel:SetAllPoints()
        keysPanel:SetFrameStrata(f:GetFrameStrata())
        keysPanel:SetFrameLevel(content:GetFrameLevel() + 1)
        keysPanel:Hide()

        local skipChatPanel = CreateFrame("Frame", nil, content)
        skipChatPanel:SetAllPoints()
        skipChatPanel:SetFrameStrata(f:GetFrameStrata())
        skipChatPanel:SetFrameLevel(content:GetFrameLevel() + 1)
        skipChatPanel:Hide()

        local groupFinderPanel = CreateFrame("Frame", nil, content)
        groupFinderPanel:SetAllPoints()
        groupFinderPanel:SetFrameStrata(f:GetFrameStrata())
        groupFinderPanel:SetFrameLevel(content:GetFrameLevel() + 1)
        groupFinderPanel:Hide()

        local trinketsPanel = CreateFrame("Frame", nil, content)
        trinketsPanel:SetAllPoints()
        trinketsPanel:SetFrameStrata(f:GetFrameStrata())
        trinketsPanel:SetFrameLevel(content:GetFrameLevel() + 1)
        trinketsPanel:Hide()

        local luraPanel = CreateFrame("Frame", nil, content)
        luraPanel:SetAllPoints()
        luraPanel:SetFrameStrata(f:GetFrameStrata())
        luraPanel:SetFrameLevel(content:GetFrameLevel() + 1)
        luraPanel:Hide()

        local teleportsTitle = teleportsPanel:CreateFontString(nil, "ARTWORK", "GameFontNormalLarge")
        teleportsTitle:SetPoint("TOP", 0, -18)
        teleportsTitle:SetText("Teleports")

        local teleportsSubtitle = teleportsPanel:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall")
        teleportsSubtitle:SetPoint("TOP", teleportsTitle, "BOTTOM", 0, -10)
        teleportsSubtitle:SetWidth(650)
        teleportsSubtitle:SetJustifyH("CENTER")
        teleportsSubtitle:SetText("")

        local teleportHost = CreateFrame("Frame", nil, teleportsPanel, "BackdropTemplate")
        teleportHost:SetPoint("TOPLEFT", 14, -98)
        teleportHost:SetPoint("BOTTOMRIGHT", teleportsPanel, "BOTTOMRIGHT", -14, 14)
        if teleportHost.SetBackdrop then
            teleportHost:SetBackdrop({
                bgFile = "Interface/ChatFrame/ChatFrameBackground",
                edgeFile = "Interface/Tooltips/UI-Tooltip-Border",
                tile = true,
                tileSize = 16,
                edgeSize = 12,
                insets = { left = 3, right = 3, top = 3, bottom = 3 },
            })
            teleportHost:SetBackdropColor(0, 0, 0, 0.55)
            teleportHost:SetBackdropBorderColor(0.65, 0.4, 0.08, 0.9)
        end

        local fallbackTitle = teleportHost:CreateFontString(nil, "ARTWORK", "GameFontHighlightLarge")
        fallbackTitle:SetPoint("CENTER", teleportHost, "CENTER", 0, 18)
        fallbackTitle:SetText("Teleport module not loaded")

        local fallbackBody = teleportHost:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall")
        fallbackBody:SetPoint("TOP", fallbackTitle, "BOTTOM", 0, -10)
        fallbackBody:SetWidth(580)
        fallbackBody:SetJustifyH("CENTER")
        fallbackBody:SetText("The base addon is still running. Add Cowmera_Teleports.lua to the addon folder and make sure it is listed in Cowmera.toc.")

        f.teleportHost = teleportHost
        f.teleportFallbackTitle = fallbackTitle
        f.teleportFallbackBody = fallbackBody
        local toolsTitle = toolsPanel:CreateFontString(nil, "ARTWORK", "GameFontNormalLarge")
        toolsTitle:SetPoint("TOP", 0, -10)
        toolsTitle:SetText("SYNC")

        local toolsSubtitle = toolsPanel:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall")
        toolsSubtitle:SetPoint("TOP", toolsTitle, "BOTTOM", 0, -8)
        toolsSubtitle:SetText("Snapshot, SavedVariables, and current sync-related tools.")

        local btnSave = CreateActionButton(toolsPanel, 120, 24, "Save Snapshot", 20, -46, function()
            local record = SaveSnapshot()
            if f.refreshOutput then f.refreshOutput(record) end
            CowmeraPrint("Saved snapshot to SavedVariables for " .. tostring(record and record.fullName or "Unknown") .. ".")
        end)

        local btnRefresh = CreateActionButton(toolsPanel, 120, 24, "Reload / Update", 148, -46, function()
            SaveSnapshot()
            CowmeraDB.ui = CowmeraDB.ui or {}
            CowmeraDB.ui.reopenAfterReload = true
            ReloadUI()
        end)

        local btnGear = CreateActionButton(toolsPanel, 90, 24, "Print Gear", 276, -46, function()
            local record = SaveSnapshot()
            if f.refreshOutput then f.refreshOutput(record) end
            PrintGear()
        end)

        local cb1 = CreateCheckButton(toolsPanel, "Auto snapshot on login", "Save a snapshot shortly after logging in.", function(v) EnsureOptions().autoSnapshotOnLogin = v end)
        cb1:SetPoint("TOPLEFT", 20, -84)

        local cb2 = CreateCheckButton(toolsPanel, "Auto snapshot on entering world", "Save after zoning or loading screens.", function(v) EnsureOptions().autoSnapshotOnEnterWorld = v end)
        cb2:SetPoint("TOPLEFT", cb1, "BOTTOMLEFT", 0, -4)

        local cb3 = CreateCheckButton(toolsPanel, "Auto snapshot on equipment changes", "Save when equipped gear changes.", function(v) EnsureOptions().autoSnapshotOnEquipment = v end)
        cb3:SetPoint("TOPLEFT", cb2, "BOTTOMLEFT", 0, -4)

        local cb7 = CreateCheckButton(toolsPanel, "Show minimap button", "Shows a draggable minimap button to open or close Cowmera.", function(v)
            EnsureButtonSettings().showMinimapButton = v
            UpdateMinimapButtonState()
        end)
        cb7:SetPoint("TOPLEFT", cb3, "BOTTOMLEFT", 0, -4)

        local divider = toolsPanel:CreateTexture(nil, "ARTWORK")
        divider:SetColorTexture(1, 0.65, 0.1, 0.2)
        divider:SetPoint("TOPLEFT", 18, -212)
        divider:SetPoint("TOPRIGHT", -18, -212)
        divider:SetHeight(1)

        local summaryLabel = toolsPanel:CreateFontString(nil, "ARTWORK", "GameFontNormal")
        summaryLabel:SetPoint("TOPLEFT", 20, -240)
        summaryLabel:SetText("Saved Snapshot")

        local summaryText = toolsPanel:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall")
        summaryText:SetPoint("TOPLEFT", summaryLabel, "BOTTOMLEFT", 0, -8)
        summaryText:SetWidth(680)
        summaryText:SetJustifyH("LEFT")
        summaryText:SetJustifyV("TOP")
        summaryText:SetText("No saved snapshot yet.")

        local outputLabel = toolsPanel:CreateFontString(nil, "ARTWORK", "GameFontNormal")
        outputLabel:SetPoint("TOPLEFT", summaryText, "BOTTOMLEFT", 0, -16)
        outputLabel:SetText("                                 ")

        local copyBtn = CreateActionButton(toolsPanel, 90, 22, "Select All", 688, -354, function() end)

        local scrollBg = CreateFrame("Frame", nil, toolsPanel, "BackdropTemplate")
        scrollBg:SetPoint("TOPLEFT", outputLabel, "BOTTOMLEFT", -6, -8)
        scrollBg:SetPoint("BOTTOMRIGHT", -18, 44)
        if scrollBg.SetBackdrop then
            scrollBg:SetBackdrop({
                bgFile = "Interface/ChatFrame/ChatFrameBackground",
                edgeFile = "Interface/Tooltips/UI-Tooltip-Border",
                tile = true,
                tileSize = 16,
                edgeSize = 12,
                insets = { left = 3, right = 3, top = 3, bottom = 3 },
            })
            scrollBg:SetBackdropColor(0, 0, 0, 0.55)
            scrollBg:SetBackdropBorderColor(0.65, 0.4, 0.08, 0.9)
        end

        local scroll = CreateFrame("ScrollFrame", "CowmeraOutputScrollFrame", scrollBg, "UIPanelScrollFrameTemplate")
        scroll:SetPoint("TOPLEFT", 8, -8)
        scroll:SetPoint("BOTTOMRIGHT", -28, 8)
        scroll:EnableMouseWheel(true)

        local output = CreateFrame("EditBox", "CowmeraOutputEditBox", scroll)
        output:SetMultiLine(true)
        output:SetAutoFocus(false)
        output:SetFontObject(ChatFontNormal)
        output:SetJustifyH("LEFT")
        output:SetJustifyV("TOP")
        output:SetTextInsets(6, 6, 6, 6)
        output:SetWidth(740)
        output:SetHeight(400)
        output:SetScript("OnEscapePressed", function(self) self:ClearFocus() end)
        output:SetScript("OnEditFocusGained", function(self) self:HighlightText() end)
        output:SetScript("OnCursorChanged", function(self, x, y, w, h)
            scroll:SetVerticalScroll(y)
        end)
        output:SetScript("OnTextChanged", function(self)
            local minHeight = scroll:GetHeight() or 210
            local lineHeight = select(2, self:GetFont()) or 14
            local numLines = 1

            if self.GetNumberOfLines then
                local ok, lines = pcall(self.GetNumberOfLines, self)
                if ok and type(lines) == "number" and lines > 0 then
                    numLines = lines
                else
                    local textValue = self:GetText() or ""
                    numLines = 1
                    for _ in textValue:gmatch("\n") do
                        numLines = numLines + 1
                    end
                end
            else
                local textValue = self:GetText() or ""
                for _ in textValue:gmatch("\n") do
                    numLines = numLines + 1
                end
            end

            local textHeight = (numLines * lineHeight) + 24
            self:SetHeight(math.max(minHeight, textHeight))
        end)
        scroll:SetScrollChild(output)

        local function CowmeraSyncScrollBy(delta)
            local current = scroll:GetVerticalScroll() or 0
            local maxScroll = scroll:GetVerticalScrollRange() or 0
            local step = 40
            scroll:SetVerticalScroll(math.max(0, math.min(current - (delta * step), maxScroll)))
        end

        scrollBg:EnableMouseWheel(true)
        scrollBg:SetScript("OnMouseWheel", function(_, delta)
            CowmeraSyncScrollBy(delta)
        end)

        scroll:SetScript("OnMouseWheel", function(_, delta)
            CowmeraSyncScrollBy(delta)
        end)

        output:EnableMouseWheel(true)
        output:SetScript("OnMouseWheel", function(_, delta)
            CowmeraSyncScrollBy(delta)
        end)

        copyBtn:ClearAllPoints()
        copyBtn:SetPoint("RIGHT", outputLabel, "RIGHT", 0, 0)
        copyBtn:SetScript("OnClick", function()
            output:SetFocus()
            output:HighlightText()
        end)

        local keysTitle = keysPanel:CreateFontString(nil, "ARTWORK", "GameFontNormalLarge")
        keysTitle:SetPoint("TOP", 0, -18)
        keysTitle:SetText("Keys")

        local keysSubtitle = keysPanel:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall")
        keysSubtitle:SetPoint("TOP", keysTitle, "BOTTOM", 0, -10)
        keysSubtitle:SetWidth(650)
        keysSubtitle:SetJustifyH("CENTER")
        keysSubtitle:SetText("")

        local keysHost = CreateFrame("Frame", nil, keysPanel, "BackdropTemplate")
        keysHost:SetPoint("TOPLEFT", 14, -98)
        keysHost:SetPoint("BOTTOMRIGHT", keysPanel, "BOTTOMRIGHT", -14, 14)
        if keysHost.SetBackdrop then
            keysHost:SetBackdrop({
                bgFile = "Interface/ChatFrame/ChatFrameBackground",
                edgeFile = "Interface/Tooltips/UI-Tooltip-Border",
                tile = true,
                tileSize = 16,
                edgeSize = 12,
                insets = { left = 3, right = 3, top = 3, bottom = 3 },
            })
            keysHost:SetBackdropColor(0, 0, 0, 0.55)
            keysHost:SetBackdropBorderColor(0.65, 0.4, 0.08, 0.9)
        end

        local keysFallbackTitle = keysHost:CreateFontString(nil, "ARTWORK", "GameFontHighlightLarge")
        keysFallbackTitle:SetPoint("CENTER", keysHost, "CENTER", 0, 18)
        keysFallbackTitle:SetText("Keys module not loaded")

        local keysFallbackBody = keysHost:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall")
        keysFallbackBody:SetPoint("TOP", keysFallbackTitle, "BOTTOM", 0, -10)
        keysFallbackBody:SetWidth(580)
        keysFallbackBody:SetJustifyH("CENTER")
        keysFallbackBody:SetText("Add Cowmera_Keys.lua to the addon folder and list it in Cowmera.toc.")

        f.keysHost = keysHost
        f.keysFallbackTitle = keysFallbackTitle
        f.keysFallbackBody = keysFallbackBody

        local skipChatTitle = skipChatPanel:CreateFontString(nil, "ARTWORK", "GameFontNormalLarge")
        skipChatTitle:SetPoint("TOP", 0, -18)
        skipChatTitle:SetText("SkipChat")

        local skipChatSubtitle = skipChatPanel:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall")
        skipChatSubtitle:SetPoint("TOP", skipChatTitle, "BOTTOM", 0, -10)
        skipChatSubtitle:SetWidth(650)
        skipChatSubtitle:SetJustifyH("CENTER")
        skipChatSubtitle:SetText("")

        local skipChatHost = CreateFrame("Frame", nil, skipChatPanel, "BackdropTemplate")
        skipChatHost:SetPoint("TOPLEFT", 14, -98)
        skipChatHost:SetPoint("BOTTOMRIGHT", skipChatPanel, "BOTTOMRIGHT", -14, 14)
        if skipChatHost.SetBackdrop then
            skipChatHost:SetBackdrop({
                bgFile = "Interface/ChatFrame/ChatFrameBackground",
                edgeFile = "Interface/Tooltips/UI-Tooltip-Border",
                tile = true,
                tileSize = 16,
                edgeSize = 12,
                insets = { left = 3, right = 3, top = 3, bottom = 3 },
            })
            skipChatHost:SetBackdropColor(0, 0, 0, 0.55)
            skipChatHost:SetBackdropBorderColor(0.65, 0.4, 0.08, 0.9)
        end

        local skipChatFallbackTitle = skipChatHost:CreateFontString(nil, "ARTWORK", "GameFontHighlightLarge")
        skipChatFallbackTitle:SetPoint("CENTER", skipChatHost, "CENTER", 0, 18)
        skipChatFallbackTitle:SetText("SkipChat module not loaded")

        local skipChatFallbackBody = skipChatHost:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall")
        skipChatFallbackBody:SetPoint("TOP", skipChatFallbackTitle, "BOTTOM", 0, -10)
        skipChatFallbackBody:SetWidth(580)
        skipChatFallbackBody:SetJustifyH("CENTER")
        skipChatFallbackBody:SetText("Add Cowmera_Dialog.lua to the addon folder and list it in Cowmera.toc.")

        f.skipChatHost = skipChatHost
        f.skipChatFallbackTitle = skipChatFallbackTitle
        f.skipChatFallbackBody = skipChatFallbackBody

        local groupFinderTitle = groupFinderPanel:CreateFontString(nil, "ARTWORK", "GameFontNormalLarge")
        groupFinderTitle:SetPoint("TOP", 0, -18)
        groupFinderTitle:SetText("GroupFinder")

        local groupFinderSubtitle = groupFinderPanel:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall")
        groupFinderSubtitle:SetPoint("TOP", groupFinderTitle, "BOTTOM", 0, -10)
        groupFinderSubtitle:SetWidth(650)
        groupFinderSubtitle:SetJustifyH("CENTER")
        groupFinderSubtitle:SetText("")

        local groupFinderHost = CreateFrame("Frame", nil, groupFinderPanel, "BackdropTemplate")
        groupFinderHost:SetPoint("TOPLEFT", 14, -98)
        groupFinderHost:SetPoint("BOTTOMRIGHT", groupFinderPanel, "BOTTOMRIGHT", -14, 14)
        if groupFinderHost.SetBackdrop then
            groupFinderHost:SetBackdrop({
                bgFile = "Interface/ChatFrame/ChatFrameBackground",
                edgeFile = "Interface/Tooltips/UI-Tooltip-Border",
                tile = true,
                tileSize = 16,
                edgeSize = 12,
                insets = { left = 3, right = 3, top = 3, bottom = 3 },
            })
            groupFinderHost:SetBackdropColor(0, 0, 0, 0.55)
            groupFinderHost:SetBackdropBorderColor(0.65, 0.4, 0.08, 0.9)
        end

        local groupFinderFallbackTitle = groupFinderHost:CreateFontString(nil, "ARTWORK", "GameFontHighlightLarge")
        groupFinderFallbackTitle:SetPoint("CENTER", groupFinderHost, "CENTER", 0, 18)
        groupFinderFallbackTitle:SetText("GroupFinder module not loaded")

        local groupFinderFallbackBody = groupFinderHost:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall")
        groupFinderFallbackBody:SetPoint("TOP", groupFinderFallbackTitle, "BOTTOM", 0, -10)
        groupFinderFallbackBody:SetWidth(580)
        groupFinderFallbackBody:SetJustifyH("CENTER")
        groupFinderFallbackBody:SetText("Add groupfinder.lua to the addon folder and list it in Cowmera.toc.")

        f.groupFinderHost = groupFinderHost
        f.groupFinderFallbackTitle = groupFinderFallbackTitle
        f.groupFinderFallbackBody = groupFinderFallbackBody

        local trinketsTitle = trinketsPanel:CreateFontString(nil, "ARTWORK", "GameFontNormalLarge")
        trinketsTitle:SetPoint("TOP", 0, -18)
        trinketsTitle:SetText("Trinkets")

        local trinketsSubtitle = trinketsPanel:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall")
        trinketsSubtitle:SetPoint("TOP", trinketsTitle, "BOTTOM", 0, -10)
        trinketsSubtitle:SetWidth(650)
        trinketsSubtitle:SetJustifyH("CENTER")
        trinketsSubtitle:SetText("")

        local trinketsHost = CreateFrame("Frame", nil, trinketsPanel, "BackdropTemplate")
        trinketsHost:SetPoint("TOPLEFT", 14, -98)
        trinketsHost:SetPoint("BOTTOMRIGHT", trinketsPanel, "BOTTOMRIGHT", -14, 14)
        if trinketsHost.SetBackdrop then
            trinketsHost:SetBackdrop({
                bgFile = "Interface/ChatFrame/ChatFrameBackground",
                edgeFile = "Interface/Tooltips/UI-Tooltip-Border",
                tile = true,
                tileSize = 16,
                edgeSize = 12,
                insets = { left = 3, right = 3, top = 3, bottom = 3 },
            })
            trinketsHost:SetBackdropColor(0, 0, 0, 0.55)
            trinketsHost:SetBackdropBorderColor(0.65, 0.4, 0.08, 0.9)
        end

        local trinketsFallbackTitle = trinketsHost:CreateFontString(nil, "ARTWORK", "GameFontHighlightLarge")
        trinketsFallbackTitle:SetPoint("CENTER", trinketsHost, "CENTER", 0, 18)
        trinketsFallbackTitle:SetText("Trinket module not loaded")

        local trinketsFallbackBody = trinketsHost:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall")
        trinketsFallbackBody:SetPoint("TOP", trinketsFallbackTitle, "BOTTOM", 0, -10)
        trinketsFallbackBody:SetWidth(580)
        trinketsFallbackBody:SetJustifyH("CENTER")
        trinketsFallbackBody:SetText("Add trinketbutton.lua to the addon folder and list it in Cowmera.toc.")

        f.trinketsHost = trinketsHost
        f.trinketsFallbackTitle = trinketsFallbackTitle
        f.trinketsFallbackBody = trinketsFallbackBody

        local luraTitle = luraPanel:CreateFontString(nil, "ARTWORK", "GameFontNormalLarge")
        luraTitle:SetPoint("TOP", 0, -18)
        luraTitle:SetText("LURA")

        local luraSubtitle = luraPanel:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall")
        luraSubtitle:SetPoint("TOP", luraTitle, "BOTTOM", 0, -10)
        luraSubtitle:SetWidth(650)
        luraSubtitle:SetJustifyH("CENTER")
        luraSubtitle:SetText("L'ura sequence helper. Raid lead and assistants can set the order; everyone can view it.")

        local luraHost = CreateFrame("Frame", nil, luraPanel, "BackdropTemplate")
        luraHost:SetPoint("TOPLEFT", 14, -98)
        luraHost:SetPoint("BOTTOMRIGHT", luraPanel, "BOTTOMRIGHT", -14, 14)
        if luraHost.SetBackdrop then
            luraHost:SetBackdrop({
                bgFile = "Interface/ChatFrame/ChatFrameBackground",
                edgeFile = "Interface/Tooltips/UI-Tooltip-Border",
                tile = true,
                tileSize = 16,
                edgeSize = 12,
                insets = { left = 3, right = 3, top = 3, bottom = 3 },
            })
            luraHost:SetBackdropColor(0, 0, 0, 0.55)
            luraHost:SetBackdropBorderColor(0.65, 0.4, 0.08, 0.9)
        end

        local luraFallbackTitle = luraHost:CreateFontString(nil, "ARTWORK", "GameFontHighlightLarge")
        luraFallbackTitle:SetPoint("CENTER", luraHost, "CENTER", 0, 18)
        luraFallbackTitle:SetText("LURA module not loaded")

        local luraFallbackBody = luraHost:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall")
        luraFallbackBody:SetPoint("TOP", luraFallbackTitle, "BOTTOM", 0, -10)
        luraFallbackBody:SetWidth(580)
        luraFallbackBody:SetJustifyH("CENTER")
        luraFallbackBody:SetText("Add Cowmera_Lura.lua to the addon folder and list it in Cowmera.toc.")

        f.luraHost = luraHost
        f.luraFallbackTitle = luraFallbackTitle
        f.luraFallbackBody = luraFallbackBody

        local navigationTitle = navigationPanel:CreateFontString(nil, "ARTWORK", "GameFontNormalLarge")
        navigationTitle:SetPoint("TOP", 0, -18)
        navigationTitle:SetText("Navigation")

        local navigationSubtitle = navigationPanel:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall")
        navigationSubtitle:SetPoint("TOP", navigationTitle, "BOTTOM", 0, -10)
        navigationSubtitle:SetWidth(650)
        navigationSubtitle:SetJustifyH("CENTER")
        navigationSubtitle:SetText("Quick-open game windows from inside Cowmera.")

        local navigationHost = CreateFrame("Frame", nil, navigationPanel, "BackdropTemplate")
        navigationHost:SetPoint("TOPLEFT", 14, -98)
        navigationHost:SetPoint("BOTTOMRIGHT", navigationPanel, "BOTTOMRIGHT", -14, 14)
        if navigationHost.SetBackdrop then
            navigationHost:SetBackdrop({
                bgFile = "Interface/ChatFrame/ChatFrameBackground",
                edgeFile = "Interface/Tooltips/UI-Tooltip-Border",
                tile = true,
                tileSize = 16,
                edgeSize = 12,
                insets = { left = 3, right = 3, top = 3, bottom = 3 },
            })
            navigationHost:SetBackdropColor(0, 0, 0, 0.55)
            navigationHost:SetBackdropBorderColor(0.65, 0.4, 0.08, 0.9)
        end

        local navigationHelp = navigationHost:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall")
        navigationHelp:SetPoint("TOP", 0, -18)
        navigationHelp:SetWidth(620)
        navigationHelp:SetJustifyH("CENTER")
        navigationHelp:SetText("")

        for index, entry in ipairs(COWMERA_NAVIGATION_BUTTONS) do
            local column = (index - 1) % 3
            local row = math.floor((index - 1) / 3)
            local btn = CreateFrame("Button", nil, navigationHost, "UIPanelButtonTemplate")
            btn:SetSize(190, 32)
            btn:SetPoint("TOPLEFT", 22 + (column * 198), -70 - (row * 42))
            btn:SetText(entry.text)
            btn:SetScript("OnClick", function()
                if entry.onClick then
                    entry.onClick()
                end
            end)
        end

        local help = toolsPanel:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall")
        help:SetPoint("BOTTOMLEFT", 20, 14)
        help:SetJustifyH("LEFT")
        help:SetText([[Commands: /cowmera, /cowmera ui, /cowmera save, /cowmera gear
Tip: click Select All, then press Ctrl+C to copy your saved snapshot text.]])

        f.panels = {
            teleports = teleportsPanel,
            keys = keysPanel,
            skipchat = skipChatPanel,
            groupfinder = groupFinderPanel,
            trinkets = trinketsPanel,
            lura = luraPanel,
            navigation = navigationPanel,
            tools = toolsPanel,
        }

        f.showPanel = function(self, panelKey)
            panelKey = panelKey or "teleports"

            self.panels.teleports:Hide()
            self.panels.keys:Hide()
            self.panels.skipchat:Hide()
            self.panels.groupfinder:Hide()
            self.panels.trinkets:Hide()
            self.panels.lura:Hide()
            self.panels.navigation:Hide()
            self.panels.tools:Hide()

            if panelKey == "tools" then
                self.panels.tools:Show()
                if self.refreshOptions then self.refreshOptions() end
                if self.refreshOutput then self:refreshOutput(GetSavedPlayerRecord()) end
            elseif panelKey == "navigation" then
                self.panels.navigation:Show()
            elseif panelKey == "groupfinder" then
                self.panels.groupfinder:Show()
                if _G.Cowmera and type(_G.Cowmera.RefreshGroupFinderPanel) == "function" then
                    _G.Cowmera.RefreshGroupFinderPanel()
                end
            elseif panelKey == "trinkets" then
                self.panels.trinkets:Show()
                if _G.Cowmera and type(_G.Cowmera.BuildTrinketPanel) == "function" then
                    _G.Cowmera.BuildTrinketPanel(self.trinketsHost, self)
                end
            elseif panelKey == "lura" then
                self.panels.lura:Show()
                if _G.Cowmera and type(_G.Cowmera.BuildLuraPanel) == "function" then
                    _G.Cowmera.BuildLuraPanel(self.luraHost, self)
                elseif type(_G.Cowmera_CreateLuraPanel) == "function" then
                    _G.Cowmera_CreateLuraPanel(self.luraHost)
                elseif _G.CowmeraLura and type(_G.CowmeraLura.Show) == "function" then
                    _G.CowmeraLura.Show(self.luraHost)
                end
            elseif panelKey == "skipchat" then
                self.panels.skipchat:Show()
                if _G.Cowmera and type(_G.Cowmera.RefreshSkipChatPanel) == "function" then
                    _G.Cowmera.RefreshSkipChatPanel()
                end
            elseif panelKey == "keys" then
                self.panels.keys:Show()
                if _G.Cowmera and type(_G.Cowmera.RefreshKeysPanel) == "function" then
                    _G.Cowmera.RefreshKeysPanel()
                end
            else
                self.panels.teleports:Show()
                if self.CowmeraRefreshTeleports then
                    self.CowmeraRefreshTeleports(true)
                elseif self.teleportHost and self.teleportHost.CowmeraRefreshTeleports then
                    self.teleportHost.CowmeraRefreshTeleports(true)
                end
            end

            self.activePanel = panelKey
            SetNavActive(panelKey)
        end

        local teleportsBtn = CreateNavButton(sidebar, "Teleports", "teleports", 18, -82)
        teleportsBtn:SetScript("OnClick", function()
            if f.activePanel ~= "teleports" then
                f:showPanel("teleports")
            end
        end)

        -- Keys tab hidden from the sidebar. Keep the Keys module loaded so any shared key/teleport helpers still exist.
        -- To restore the tab later, re-add this button block and move the tabs below back down by 34 pixels.

        local skipChatBtn = CreateNavButton(sidebar, "SkipChat", "skipchat", 18, -116)
        skipChatBtn:SetScript("OnClick", function()
            if f.activePanel ~= "skipchat" then
                f:showPanel("skipchat")
            end
        end)

        local groupFinderBtn = CreateNavButton(sidebar, "GroupFinder", "groupfinder", 18, -150)
        groupFinderBtn:SetScript("OnClick", function()
            if f.activePanel ~= "groupfinder" then
                f:showPanel("groupfinder")
            end
        end)

        local trinketsBtn = CreateNavButton(sidebar, "Trinkets", "trinkets", 18, -184)
        trinketsBtn:SetScript("OnClick", function()
            if f.activePanel ~= "trinkets" then
                f:showPanel("trinkets")
            end
        end)

        local luraBtn = CreateNavButton(sidebar, "LURA", "lura", 18, -218)
        luraBtn:SetScript("OnClick", function()
            if f.activePanel ~= "lura" then
                f:showPanel("lura")
            end
        end)

        local navigationBtn = CreateNavButton(sidebar, "Navigation", "navigation", 18, -252)
        navigationBtn:SetScript("OnClick", function()
            if f.activePanel ~= "navigation" then
                f:showPanel("navigation")
            end
        end)

        local toolsBtn = CreateNavButton(sidebar, "SYNC", "tools", 18, -286)
        toolsBtn:SetScript("OnClick", function()
            if f.activePanel ~= "tools" then
                f:showPanel("tools")
            end
        end)

        f.refreshOutput = function(_, record)
            local saved = record or GetSavedPlayerRecord() or SaveSnapshot()
            summaryText:SetText(BuildSavedGearSummary(saved))
            output:SetText(BuildSavedOutputText(saved))
            output:ClearFocus()
            output:SetCursorPosition(0)
            scroll:SetVerticalScroll(0)
        end

        f.refreshOptions = function()
            local o = EnsureOptions()
            cb1:SetChecked(o.autoSnapshotOnLogin)
            cb2:SetChecked(o.autoSnapshotOnEnterWorld)
            cb3:SetChecked(o.autoSnapshotOnEquipment)
            cb7:SetChecked(EnsureButtonSettings().showMinimapButton)
        end

        if _G.Cowmera and type(_G.Cowmera.BuildTeleportPanel) == "function" then
            local ok, err = pcall(_G.Cowmera.BuildTeleportPanel, f.teleportHost, f)
            if ok then
                f.teleportModuleLoaded = true
                if f.teleportFallbackTitle then f.teleportFallbackTitle:Hide() end
                if f.teleportFallbackBody then f.teleportFallbackBody:Hide() end
            else
                if f.teleportFallbackBody then
                    f.teleportFallbackBody:SetText("Teleport module failed to load: " .. tostring(err))
                end
            end
        end
        if _G.Cowmera and type(_G.Cowmera.BuildKeysPanel) == "function" then
            local ok, err = pcall(_G.Cowmera.BuildKeysPanel, f.keysHost, f)
            if ok then
                f.keysModuleLoaded = true
                if f.keysFallbackTitle then f.keysFallbackTitle:Hide() end
                if f.keysFallbackBody then f.keysFallbackBody:Hide() end
            else
                if f.keysFallbackBody then
                    f.keysFallbackBody:SetText("Keys module failed to load: " .. tostring(err))
                end
            end
        end
        if _G.Cowmera and type(_G.Cowmera.BuildSkipChatPanel) == "function" then
            local ok, err = pcall(_G.Cowmera.BuildSkipChatPanel, f.skipChatHost, f)
            if ok then
                f.skipChatModuleLoaded = true
                if f.skipChatFallbackTitle then f.skipChatFallbackTitle:Hide() end
                if f.skipChatFallbackBody then f.skipChatFallbackBody:Hide() end
            else
                if f.skipChatFallbackBody then
                    f.skipChatFallbackBody:SetText("SkipChat module failed to load: " .. tostring(err))
                end
            end
        end
        if _G.Cowmera and type(_G.Cowmera.BuildGroupFinderPanel) == "function" then
            local ok, err = pcall(_G.Cowmera.BuildGroupFinderPanel, f.groupFinderHost, f)
            if ok then
                f.groupFinderModuleLoaded = true
                if f.groupFinderFallbackTitle then f.groupFinderFallbackTitle:Hide() end
                if f.groupFinderFallbackBody then f.groupFinderFallbackBody:Hide() end
            else
                if f.groupFinderFallbackBody then
                    f.groupFinderFallbackBody:SetText("GroupFinder module failed to load: " .. tostring(err))
                end
            end
        end
        if _G.Cowmera and type(_G.Cowmera.BuildTrinketPanel) == "function" then
            local ok, err = pcall(_G.Cowmera.BuildTrinketPanel, f.trinketsHost, f)
            if ok then
                f.trinketModuleLoaded = true
                if f.trinketsFallbackTitle then f.trinketsFallbackTitle:Hide() end
                if f.trinketsFallbackBody then f.trinketsFallbackBody:Hide() end
            else
                if f.trinketsFallbackBody then
                    f.trinketsFallbackBody:SetText("Trinket module failed to load: " .. tostring(err))
                end
            end
        end
        local luraBuilder = (_G.Cowmera and type(_G.Cowmera.BuildLuraPanel) == "function" and _G.Cowmera.BuildLuraPanel)
            or (type(_G.Cowmera_CreateLuraPanel) == "function" and _G.Cowmera_CreateLuraPanel)
            or (_G.CowmeraLura and type(_G.CowmeraLura.Show) == "function" and _G.CowmeraLura.Show)
        if luraBuilder then
            local ok, err = pcall(luraBuilder, f.luraHost, f)
            if ok then
                f.luraModuleLoaded = true
                if f.luraFallbackTitle then f.luraFallbackTitle:Hide() end
                if f.luraFallbackBody then f.luraFallbackBody:Hide() end
            else
                if f.luraFallbackBody then
                    f.luraFallbackBody:SetText("LURA module failed to load: " .. tostring(err))
                end
            end
        end
        f:showPanel("teleports")

        CowmeraUI = f
    end

    CowmeraUI:Show()
    CowmeraUI:Raise()
    if _G.Cowmera and type(_G.Cowmera.BuildTeleportPanel) == "function" and CowmeraUI.teleportHost and not CowmeraUI.teleportModuleLoaded then
        local ok, err = pcall(_G.Cowmera.BuildTeleportPanel, CowmeraUI.teleportHost, CowmeraUI)
        if ok then
            CowmeraUI.teleportModuleLoaded = true
            if CowmeraUI.teleportFallbackTitle then CowmeraUI.teleportFallbackTitle:Hide() end
            if CowmeraUI.teleportFallbackBody then CowmeraUI.teleportFallbackBody:Hide() end
        else
            if CowmeraUI.teleportFallbackBody then
                CowmeraUI.teleportFallbackBody:SetText("Teleport module failed to load: " .. tostring(err))
            end
        end
    end
    if _G.Cowmera and type(_G.Cowmera.BuildKeysPanel) == "function" and CowmeraUI.keysHost and not CowmeraUI.keysModuleLoaded then
        local ok, err = pcall(_G.Cowmera.BuildKeysPanel, CowmeraUI.keysHost, CowmeraUI)
        if ok then
            CowmeraUI.keysModuleLoaded = true
            if CowmeraUI.keysFallbackTitle then CowmeraUI.keysFallbackTitle:Hide() end
            if CowmeraUI.keysFallbackBody then CowmeraUI.keysFallbackBody:Hide() end
        else
            if CowmeraUI.keysFallbackBody then
                CowmeraUI.keysFallbackBody:SetText("Keys module failed to load: " .. tostring(err))
            end
        end
    end
    if _G.Cowmera and type(_G.Cowmera.BuildSkipChatPanel) == "function" and CowmeraUI.skipChatHost and not CowmeraUI.skipChatModuleLoaded then
        local ok, err = pcall(_G.Cowmera.BuildSkipChatPanel, CowmeraUI.skipChatHost, CowmeraUI)
        if ok then
            CowmeraUI.skipChatModuleLoaded = true
            if CowmeraUI.skipChatFallbackTitle then CowmeraUI.skipChatFallbackTitle:Hide() end
            if CowmeraUI.skipChatFallbackBody then CowmeraUI.skipChatFallbackBody:Hide() end
        else
            if CowmeraUI.skipChatFallbackBody then
                CowmeraUI.skipChatFallbackBody:SetText("SkipChat module failed to load: " .. tostring(err))
            end
        end
    end
    if _G.Cowmera and type(_G.Cowmera.BuildGroupFinderPanel) == "function" and CowmeraUI.groupFinderHost and not CowmeraUI.groupFinderModuleLoaded then
        local ok, err = pcall(_G.Cowmera.BuildGroupFinderPanel, CowmeraUI.groupFinderHost, CowmeraUI)
        if ok then
            CowmeraUI.groupFinderModuleLoaded = true
            if CowmeraUI.groupFinderFallbackTitle then CowmeraUI.groupFinderFallbackTitle:Hide() end
            if CowmeraUI.groupFinderFallbackBody then CowmeraUI.groupFinderFallbackBody:Hide() end
        else
            if CowmeraUI.groupFinderFallbackBody then
                CowmeraUI.groupFinderFallbackBody:SetText("GroupFinder module failed to load: " .. tostring(err))
            end
        end
    end
    if _G.Cowmera and type(_G.Cowmera.BuildTrinketPanel) == "function" and CowmeraUI.trinketsHost and not CowmeraUI.trinketModuleLoaded then
        local ok, err = pcall(_G.Cowmera.BuildTrinketPanel, CowmeraUI.trinketsHost, CowmeraUI)
        if ok then
            CowmeraUI.trinketModuleLoaded = true
            if CowmeraUI.trinketsFallbackTitle then CowmeraUI.trinketsFallbackTitle:Hide() end
            if CowmeraUI.trinketsFallbackBody then CowmeraUI.trinketsFallbackBody:Hide() end
        else
            if CowmeraUI.trinketsFallbackBody then
                CowmeraUI.trinketsFallbackBody:SetText("Trinket module failed to load: " .. tostring(err))
            end
        end
    end
    local luraBuilder = (_G.Cowmera and type(_G.Cowmera.BuildLuraPanel) == "function" and _G.Cowmera.BuildLuraPanel)
        or (type(_G.Cowmera_CreateLuraPanel) == "function" and _G.Cowmera_CreateLuraPanel)
        or (_G.CowmeraLura and type(_G.CowmeraLura.Show) == "function" and _G.CowmeraLura.Show)
    if luraBuilder and CowmeraUI.luraHost and not CowmeraUI.luraModuleLoaded then
        local ok, err = pcall(luraBuilder, CowmeraUI.luraHost, CowmeraUI)
        if ok then
            CowmeraUI.luraModuleLoaded = true
            if CowmeraUI.luraFallbackTitle then CowmeraUI.luraFallbackTitle:Hide() end
            if CowmeraUI.luraFallbackBody then CowmeraUI.luraFallbackBody:Hide() end
        else
            if CowmeraUI.luraFallbackBody then
                CowmeraUI.luraFallbackBody:SetText("LURA module failed to load: " .. tostring(err))
            end
        end
    end
    if CowmeraUI.showPanel then CowmeraUI:showPanel(CowmeraUI.activePanel or "teleports") end
end

local frame = CreateFrame("Frame")
frame:RegisterEvent("PLAYER_LOGIN")
frame:RegisterEvent("PLAYER_LOGOUT")
frame:RegisterEvent("PLAYER_ENTERING_WORLD")
frame:RegisterEvent("PLAYER_EQUIPMENT_CHANGED")

local snapshotTimer = nil
local function QueueSnapshot(delay)
    if snapshotTimer then
        snapshotTimer:Cancel()
        snapshotTimer = nil
    end
    snapshotTimer = C_Timer.NewTimer(delay or 0.75, function()
        snapshotTimer = nil
        SaveSnapshot()
    end)
end

frame:SetScript("OnEvent", function(_, event, ...)
    local opts = EnsureOptions()
    if event == "PLAYER_LOGIN" then
        CowmeraDB.players = CowmeraDB.players or {}
        EnsureOptions()
        EnsureMinimapButton()
        C_Timer.After(0.5, function()
            local ui = CowmeraDB and CowmeraDB.ui
            if ui and ui.reopenAfterReload then
                ui.reopenAfterReload = nil
                ToggleCowmeraUI()
            end
        end)
        if opts.autoSnapshotOnLogin then QueueSnapshot(2.0) end
    elseif event == "PLAYER_ENTERING_WORLD" then
        if opts.autoSnapshotOnEnterWorld then QueueSnapshot(2.0) end
    elseif event == "PLAYER_EQUIPMENT_CHANGED" then
        if opts.autoSnapshotOnEquipment then QueueSnapshot(0.75) end
    elseif event == "PLAYER_LOGOUT" then
        SaveSnapshot()
    end
end)

SLASH_COWMERA1 = "/cowmera"
SLASH_COWMERA2 = "/cwm"
SlashCmdList["COWMERA"] = function(msg)
    msg = string.lower((msg or ""):gsub("^%s+", ""):gsub("%s+$", ""))

    if msg == "gear" then
        PrintGear()
    elseif msg == "save" or msg == "refresh" then
        local record = SaveSnapshot()
        CowmeraPrint("Saved snapshot to SavedVariables for " .. tostring(record and record.fullName or "Unknown") .. ".")
    elseif msg == "ui" or msg == "options" or msg == "config" or msg == "menu" or msg == "settings" or msg == "" then
        ToggleCowmeraUI()
    else
        CowmeraPrint("Commands:")
        CowmeraPrint("/cowmera ui")
        CowmeraPrint("/cowmera gear")
        CowmeraPrint("/cowmera save")
    end
end
