-- AddOn Sets backend for Dynamic Profiles.
-- When BetterAddonList is loaded, prefer its sets/API.
-- Otherwise use DynamicProfilesDB.addonSets (native).

local addonName, DP = ...
DP = DP or _G.DynamicProfiles or {}
DP.AddonSets = DP.AddonSets or {}

local AS = DP.AddonSets

local function Print(msg)
    if DP.Print then DP.Print(msg) else print("|cFF00CCFFDynamic Profiles:|r " .. tostring(msg)) end
end

local function GetDB()
    return DP.GetDB and DP.GetDB() or DynamicProfilesDB
end

local function EnsureNativeShape()
    local db = GetDB()
    if not db then return nil end
    db.addonSets = db.addonSets or {}
    db.protected = db.protected or {}
    db.protected[addonName] = true
    return db
end

function AS.UsingBAL()
    return type(_G.BetterAddonList) == "table"
        and type(_G.BetterAddonList.GetSets) == "function"
end

local function BAL()
    return _G.BetterAddonList
end

local function GetNumAddOns()
    if C_AddOns and C_AddOns.GetNumAddOns then return C_AddOns.GetNumAddOns() end
    return GetNumAddOns and GetNumAddOns() or 0
end

local function GetAddOnInfo(i)
    if C_AddOns and C_AddOns.GetAddOnInfo then
        return C_AddOns.GetAddOnInfo(i)
    end
    return GetAddOnInfo(i)
end

-- Scope enable toggles to this character. Passing nil ("all chars") is the
-- glitchy Blizzard path that can flip states across reloads even when the
-- set already matches — same fix BetterAddonList applies.
local function Character()
    return UnitGUID("player")
end

local function GetEnableState(i)
    if C_AddOns and C_AddOns.GetAddOnEnableState then
        return C_AddOns.GetAddOnEnableState(i, Character()) > 0
    end
    local _, _, _, enabled = GetAddOnInfo(i)
    return not not enabled
end

local function EnableAddon(i)
    if C_AddOns and C_AddOns.EnableAddOn then
        C_AddOns.EnableAddOn(i, Character())
    else
        EnableAddOn(i)
    end
end

local function DisableAddon(i)
    if C_AddOns and C_AddOns.DisableAddOn then
        C_AddOns.DisableAddOn(i, Character())
    else
        DisableAddOn(i)
    end
end

local function DisableAllAddOns()
    if C_AddOns and C_AddOns.DisableAllAddOns then
        C_AddOns.DisableAllAddOns()
    elseif DisableAllAddOns then
        DisableAllAddOns()
    end
end

local function GetAddOnMetadata(name, field)
    if C_AddOns and C_AddOns.GetAddOnMetadata then
        return C_AddOns.GetAddOnMetadata(name, field)
    end
    if _G.GetAddOnMetadata then return _G.GetAddOnMetadata(name, field) end
    return nil
end

local function GetAddOnDeps(name)
    if C_AddOns and C_AddOns.GetAddOnDependencies then
        return C_AddOns.GetAddOnDependencies(name)
    end
    if _G.GetAddOnDependencies then return _G.GetAddOnDependencies(name) end
    return nil
end

-------------------------------------------------------------------------------
-- Protection (native; BAL uses its own)
-------------------------------------------------------------------------------

function AS.IsProtected(addonFolderName)
    if not addonFolderName then return false end
    if addonFolderName == addonName then return true end
    -- Never disable BetterAddonList itself when a local set is applied while BAL is present.
    if addonFolderName == "BetterAddonList" then return true end
    if AS.UsingBAL() then
        -- BAL protects itself and its protected list; we only treat DP as protected here.
        return false
    end
    local db = EnsureNativeShape()
    return db and db.protected and db.protected[addonFolderName] and true or false
end

function AS.SetProtected(addonFolderName, value)
    if AS.UsingBAL() then return false end
    if not addonFolderName or addonFolderName == addonName then return false end
    local db = EnsureNativeShape()
    if not db then return false end
    if value then
        db.protected[addonFolderName] = true
    else
        db.protected[addonFolderName] = nil
    end
    return true
end

-------------------------------------------------------------------------------
-- List / members
-------------------------------------------------------------------------------

function AS.GetLocalNames()
    local db = EnsureNativeShape()
    local list = {}
    if not db then return list end
    for name in pairs(db.addonSets) do
        table.insert(list, name)
    end
    table.sort(list, function(a, b) return a:lower() < b:lower() end)
    return list
end

function AS.GetNames()
    if AS.UsingBAL() then
        local ok, list = pcall(function() return BAL():GetSets() end)
        if ok and type(list) == "table" then
            return list
        end
        return {}
    end
    return AS.GetLocalNames()
end

--- Entries for profile picker: BAL sets first (when present), then local sets.
--- Each entry: { name = string, source = "bal"|"local", label = string }
function AS.GetPickerEntries()
    local entries = {}
    if AS.UsingBAL() then
        for _, name in ipairs(AS.GetNames()) do
            table.insert(entries, { name = name, source = "bal", label = name })
        end
        for _, name in ipairs(AS.GetLocalNames()) do
            table.insert(entries, {
                name = name,
                source = "local",
                label = name .. " (Local)",
            })
        end
    else
        for _, name in ipairs(AS.GetLocalNames()) do
            table.insert(entries, { name = name, source = "local", label = name })
        end
    end
    return entries
end

function AS.GetMembers(setName)
    if not setName or setName == "" then return {} end
    if AS.UsingBAL() then
        -- Prefer BAL DB for BAL-owned set names
        local balDB = _G.BetterAddonListDB
        if balDB and balDB.sets and balDB.sets[setName] then
            local copy = {}
            for i, n in ipairs(balDB.sets[setName]) do
                copy[i] = n
            end
            return copy
        end
        -- Fall through to local if it's a local-only set while BAL is present
    end
    local db = EnsureNativeShape()
    local set = db and db.addonSets and db.addonSets[setName]
    if not set then return {} end
    local copy = {}
    for i, n in ipairs(set) do
        copy[i] = n
    end
    return copy
end

function AS.Exists(setName)
    if not setName or setName == "" then return false end
    if AS.UsingBAL() then
        local balDB = _G.BetterAddonListDB
        if balDB and balDB.sets and balDB.sets[setName] then return true end
    end
    local db = EnsureNativeShape()
    return db and db.addonSets and db.addonSets[setName] ~= nil
end

function AS.GetSource(setName)
    if not setName then return nil end
    if AS.UsingBAL() then
        local balDB = _G.BetterAddonListDB
        if balDB and balDB.sets and balDB.sets[setName] then return "bal" end
    end
    local db = EnsureNativeShape()
    if db and db.addonSets and db.addonSets[setName] then return "local" end
    return nil
end

-------------------------------------------------------------------------------
-- Mutate
-------------------------------------------------------------------------------

function AS.Save(setName, members)
    if not setName or setName == "" then return false end
    if AS.UsingBAL() and AS.GetSource(setName) ~= "local" then
        -- BAL SaveSet snapshots currently-enabled addons (no member list API).
        local ok = pcall(function() BAL():SaveSet(setName) end)
        return ok
    end
    local db = EnsureNativeShape()
    if not db then return false end
    local list = {}
    if type(members) == "table" then
        local seen = {}
        for _, name in ipairs(members) do
            if type(name) == "string" and name ~= "" and name ~= addonName and not seen[name] then
                seen[name] = true
                table.insert(list, name)
            end
        end
        table.sort(list, function(a, b) return a:lower() < b:lower() end)
    end
    db.addonSets[setName] = list
    return true
end

function AS.SaveFromCurrentEnabled(setName)
    if not setName or setName == "" then return false end
    if AS.UsingBAL() and AS.GetSource(setName) ~= "local" then
        local ok = pcall(function() BAL():SaveSet(setName) end)
        return ok
    end
    local members = {}
    for i = 1, GetNumAddOns() do
        local name = GetAddOnInfo(i)
        if name and GetEnableState(i) and not AS.IsProtected(name) then
            table.insert(members, name)
        end
    end
    return AS.Save(setName, members)
end

function AS.Create(setName)
    if not setName or setName == "" then return false end
    if AS.Exists(setName) then return false end
    if AS.UsingBAL() then
        -- Create empty BAL set by saving current enabled as starting point, or empty via DB
        local balDB = _G.BetterAddonListDB
        if balDB and balDB.sets then
            balDB.sets[setName] = {}
            return true
        end
        local ok = pcall(function() BAL():SaveSet(setName) end)
        return ok
    end
    return AS.Save(setName, {})
end

function AS.Rename(oldName, newName)
    if not oldName or not newName or newName == "" then return false end
    if oldName == newName then return true end
    if AS.Exists(newName) then return false end
    local source = AS.GetSource(oldName)
    if source == "bal" then
        local ok = pcall(function() BAL():RenameSet(oldName, newName) end)
        return ok
    end
    local db = EnsureNativeShape()
    if not db or not db.addonSets[oldName] then return false end
    db.addonSets[newName] = db.addonSets[oldName]
    db.addonSets[oldName] = nil
    return true
end

function AS.Delete(setName)
    if not setName then return false end
    local source = AS.GetSource(setName)
    if source == "bal" then
        local ok = pcall(function() BAL():DeleteSet(setName) end)
        return ok
    end
    local db = EnsureNativeShape()
    if not db or not db.addonSets[setName] then return false end
    db.addonSets[setName] = nil
    return true
end

-------------------------------------------------------------------------------
-- Apply
-------------------------------------------------------------------------------

local function MembersToLookup(members)
    local lookup = {}
    for _, name in ipairs(members or {}) do
        lookup[name] = true
    end
    lookup[addonName] = true
    return lookup
end

--- mode: "merge" = enable listed, leave others; "load" = disable non-protected then enable listed
function AS.Apply(setName, mode, verbose)
    if not setName or setName == "" then return false end
    mode = mode or "merge"

    local source = AS.GetSource(setName)
    if source == "bal" and AS.UsingBAL() then
        local ok, err
        if mode == "load" then
            ok, err = pcall(function() BAL():LoadSet(setName) end)
        else
            ok, err = pcall(function() BAL():EnableSet(setName) end)
        end
        if not ok then
            if verbose then Print("BetterAddonList apply failed: " .. tostring(err)) end
            return false
        end
        return true -- caller decides reload popup by checking enable diffs if needed
    end

    local members = AS.GetMembers(setName)
    local want = MembersToLookup(members)
    local changed = false

    if mode == "load" then
        for i = 1, GetNumAddOns() do
            local name = GetAddOnInfo(i)
            if name and not AS.IsProtected(name) and not want[name] then
                if GetEnableState(i) then
                    DisableAddon(i)
                    changed = true
                end
            end
        end
    end

    for i = 1, GetNumAddOns() do
        local name = GetAddOnInfo(i)
        if name and want[name] then
            if not GetEnableState(i) then
                EnableAddon(i)
                changed = true
            end
        end
    end

    -- Always keep ourselves enabled
    for i = 1, GetNumAddOns() do
        local name = GetAddOnInfo(i)
        if name == addonName and not GetEnableState(i) then
            EnableAddon(i)
            changed = true
        end
    end

    return changed
end

--- Convert a legacy { [name] = bool } map into a members array (enabled only).
function AS.MembersFromLegacyMap(addonSetMap)
    local members = {}
    if type(addonSetMap) ~= "table" then return members end
    for name, enabled in pairs(addonSetMap) do
        if enabled and type(name) == "string" then
            table.insert(members, name)
        end
    end
    table.sort(members, function(a, b) return a:lower() < b:lower() end)
    return members
end

--- Installed addons list for checkbox UI: { { name, title }, ... } sorted by title.
--- Dynamic Profiles itself is omitted — it is always protected and cannot be toggled in a set.
function AS.GetInstalledAddOns()
    local list = {}
    for i = 1, GetNumAddOns() do
        local name, title = GetAddOnInfo(i)
        if name and name ~= addonName then
            table.insert(list, {
                name = name,
                title = title or name,
                index = i,
            })
        end
    end
    table.sort(list, function(a, b)
        return (a.title or a.name):lower() < (b.title or b.name):lower()
    end)
    return list
end

-- Known suites whose TOC metadata does not describe the intended family.
-- Keep these narrow and explicit; normal addons should use their Group metadata
-- or dependency relationship below.
local GROUP_EXCEPTIONS = {
    {
        matches = function(name)
            return name == "LittleWigs" or name:match("^LittleWigs_") ~= nil
        end,
        key = "LittleWigs",
    },
}

--- Determine a grouping key for an addon: apply narrow suite exceptions,
--- then prefer the TOC "Group" directive, then fall back to dependencies.
local function GroupKeyFor(name, installedSet)
    for _, exception in ipairs(GROUP_EXCEPTIONS) do
        if exception.matches(name) and installedSet[exception.key] then
            return exception.key
        end
    end

    local group = GetAddOnMetadata(name, "Group")
    if group and group ~= "" then return group end

    -- Fall back to the first required dependency that is itself installed.
    local deps = { GetAddOnDeps(name) }
    for _, dep in ipairs(deps) do
        if dep and dep ~= "" and installedSet[dep] then
            return dep
        end
    end
    return nil
end

--- Installed addons organized into collapsible groups for the UI.
--- Returns an ordered array: { { key, title, addons = { {name,title,index}, ... } }, ... }
--- Real groups (2+ members) come first (alphabetical), then an "Other AddOns" bucket.
function AS.GetGroupedAddOns()
    local installed = AS.GetInstalledAddOns()
    local installedSet = {}
    for _, info in ipairs(installed) do installedSet[info.name] = info end

    local keyOf, keyCount = {}, {}
    for _, info in ipairs(installed) do
        local key = GroupKeyFor(info.name, installedSet)
        keyOf[info.name] = key
        if key then keyCount[key] = (keyCount[key] or 0) + 1 end
    end
    -- A parent addon whose own name is used as a group key by its children
    -- should join that same group.
    for _, info in ipairs(installed) do
        if not keyOf[info.name] and keyCount[info.name] then
            keyOf[info.name] = info.name
            keyCount[info.name] = keyCount[info.name] + 1
        end
    end

    local groups = {}
    local other = { key = "__other", title = "Other AddOns", addons = {}, isOther = true }
    for _, info in ipairs(installed) do
        local key = keyOf[info.name]
        if key and (keyCount[key] or 0) >= 2 then
            local g = groups[key]
            if not g then
                local title = key
                if installedSet[key] then title = installedSet[key].title or key end
                g = { key = key, title = title, addons = {} }
                groups[key] = g
            end
            table.insert(g.addons, info)
        else
            table.insert(other.addons, info)
        end
    end

    local ordered = {}
    for _, g in pairs(groups) do table.insert(ordered, g) end
    table.sort(ordered, function(a, b)
        return (a.title or a.key):lower() < (b.title or b.key):lower()
    end)
    if #other.addons > 0 then table.insert(ordered, other) end
    return ordered
end

function AS.Init()
    EnsureNativeShape()
end

DP.AddonSets = AS
