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

local FEATURE_CATEGORIES = {
    {
        key = "progress",
        label = function() return L.FEATURE_CATEGORY_PROGRESS or "Progress Tweaks" end,
        description = function() return L.FEATURE_CATEGORY_PROGRESS_DESC or "Cleaner progress displays for live systems." end,
    },
    {
        key = "character",
        label = function() return L.FEATURE_CATEGORY_CHARACTER or "Character UI" end,
        description = function() return L.FEATURE_CATEGORY_CHARACTER_DESC or "Character frame helpers and gear context." end,
    },
    {
        key = "inventory",
        label = function() return L.FEATURE_CATEGORY_INVENTORY or "Inventory" end,
        description = function() return L.FEATURE_CATEGORY_INVENTORY_DESC or "Bag, bank, and item storage helpers." end,
    },
    {
        key = "crafting",
        label = function() return L.FEATURE_CATEGORY_CRAFTING or "Crafting" end,
        description = function() return L.FEATURE_CATEGORY_CRAFTING_DESC or "Recipes, materials, and shopping plans." end,
    },
    {
        key = "quick_tools",
        label = function() return L.FEATURE_CATEGORY_QUICK_TOOLS or "Quick Tools" end,
        description = function() return L.FEATURE_CATEGORY_QUICK_TOOLS_DESC or "Fast access tools and compact utility widgets." end,
    },
    {
        key = "world_map",
        label = function() return L.FEATURE_CATEGORY_WORLD_MAP or "World Map" end,
        description = function() return L.FEATURE_CATEGORY_WORLD_MAP_DESC or "Pins, markers, and tracking helpers for the Blizzard world map." end,
    },
    {
        key = "blizzard",
        label = function() return L.FEATURE_CATEGORY_BLIZZARD or "Blizzard UI" end,
        description = function() return L.FEATURE_CATEGORY_BLIZZARD_DESC or "Focused upgrades for Blizzard screens." end,
    },
    {
        key = "layout",
        label = function() return L.FEATURE_CATEGORY_LAYOUT or "Layout & Scale" end,
        description = function() return L.FEATURE_CATEGORY_LAYOUT_DESC or "Extra layout and sizing tools." end,
    },
    {
        key = "experimental",
        label = function() return L.FEATURE_CATEGORY_EXPERIMENTAL or "Experimental" end,
        description = function() return L.FEATURE_CATEGORY_EXPERIMENTAL_DESC or "Early ideas that still need more testing." end,
    },
}

local FEATURE_ORDER = {
    "prey_bar",
    "prey_hunt_icons",
    "midnight_rare_map_pins",
    "midnight_treasure_map_pins",
    "gathering_nodes",
    "profession_knowledge_badges",
    "spec_switcher",
    "travel_bar",
    "gear_item_level",
    "inventory_tracker",
    "item_id_tooltip",
    "auto_sell_junk",
    "shopping_list",
    "bag_item_level",
    "one_click_processing",
    "auto_accept_quests",
    "gear_socket_enchant",
    "stat_focus",
    "vaultloom_armory",
    "merchant_filters",
    "blizzard_window_mover",
}

local FEATURE_DEFINITIONS = {}
local FEATURE_ORDER_INDEX = {}
for index, featureID in ipairs(FEATURE_ORDER) do
    FEATURE_ORDER_INDEX[featureID] = index
end

local CHARACTER_GEAR_SLOT_NAMES = {
    "HeadSlot",
    "NeckSlot",
    "ShoulderSlot",
    "BackSlot",
    "ChestSlot",
    "WristSlot",
    "HandsSlot",
    "WaistSlot",
    "LegsSlot",
    "FeetSlot",
    "Finger0Slot",
    "Finger1Slot",
    "Trinket0Slot",
    "Trinket1Slot",
    "MainHandSlot",
    "SecondaryHandSlot",
}

function ns:RegisterFeatureDefinition(featureID, definition)
    if type(featureID) ~= "string" or featureID == "" or type(definition) ~= "table" then
        return nil
    end

    definition.id = definition.id or featureID
    FEATURE_DEFINITIONS[featureID] = definition

    if not FEATURE_ORDER_INDEX[featureID] then
        FEATURE_ORDER[#FEATURE_ORDER + 1] = featureID
        FEATURE_ORDER_INDEX[featureID] = #FEATURE_ORDER
    end

    return definition
end

local function resolveText(value)
    if type(value) == "function" then
        return value()
    end
    return value
end

function ns:ClampNumber(value, minValue, maxValue)
    value = tonumber(value) or 0
    if value < minValue then
        return minValue
    end
    if value > maxValue then
        return maxValue
    end
    return value
end

function ns:ShowFeatureError(message)
    if type(message) ~= "string" or message == "" then
        return
    end

    if UIErrorsFrame and type(UIErrorsFrame.AddMessage) == "function" then
        UIErrorsFrame:AddMessage(message, 1, 0.18, 0.18, 1)
        return
    end

    if self.Print then
        self:Print(message)
    end
end

function ns:ShouldOpenMenuUpward(menu, anchorButton)
    if not menu or not anchorButton or not anchorButton.GetBottom or not anchorButton.GetTop then
        return false
    end

    local screenHeight = type(GetScreenHeight) == "function" and GetScreenHeight() or nil
    local anchorBottom = anchorButton:GetBottom()
    local anchorTop = anchorButton:GetTop()
    local menuHeight = menu:GetHeight()
    if not screenHeight or not anchorBottom or not anchorTop or not menuHeight then
        return false
    end

    local scale = type(anchorButton.GetEffectiveScale) == "function" and anchorButton:GetEffectiveScale() or 1
    local availableBelow = anchorBottom * scale
    local availableAbove = screenHeight - (anchorTop * scale)
    local requiredHeight = menuHeight * scale + 16

    return availableBelow < requiredHeight and availableAbove > availableBelow
end

function ns:GetCharacterGearSlotNames()
    return CHARACTER_GEAR_SLOT_NAMES
end

function ns:GetCharacterGearSlotButton(slotName)
    if type(slotName) ~= "string" or slotName == "" then
        return nil
    end

    return _G["Character" .. slotName]
end

function ns:EnsureFeatureDB()
    if not self.db and self.EnsureDB then
        self:EnsureDB()
    end

    local db = self.db
    if type(db) ~= "table" then
        return nil
    end

    db.features = type(db.features) == "table" and db.features or {}
    return db.features
end

function ns:GetFeaturePosition(positionKey, defaultPosition)
    defaultPosition = type(defaultPosition) == "table" and defaultPosition or {}

    local db = self:EnsureFeatureDB()
    local position = type(positionKey) == "string"
        and positionKey ~= ""
        and type(db) == "table"
        and type(db[positionKey]) == "table"
        and db[positionKey]
        or nil
    if not position then
        position = defaultPosition
    end

    return {
        point = type(position.point) == "string" and position.point or defaultPosition.point or "TOP",
        relativePoint = type(position.relativePoint) == "string" and position.relativePoint or defaultPosition.relativePoint or defaultPosition.point or "TOP",
        x = tonumber(position.x) or tonumber(defaultPosition.x) or 0,
        y = tonumber(position.y) or tonumber(defaultPosition.y) or 0,
    }
end

function ns:ApplyFeaturePosition(frame, positionKey, defaultPosition)
    if not frame then
        return
    end

    local position = self:GetFeaturePosition(positionKey, defaultPosition)
    frame:ClearAllPoints()
    frame:SetPoint(position.point, UIParent, position.relativePoint, position.x, position.y)
end

function ns:StoreFeaturePosition(frame, positionKey)
    if not frame or type(positionKey) ~= "string" or positionKey == "" then
        return
    end

    local point, _, relativePoint, x, y = frame:GetPoint(1)
    if not point then
        return
    end

    local db = self:EnsureFeatureDB()
    if type(db) ~= "table" then
        return
    end

    db[positionKey] = {
        point = point,
        relativePoint = relativePoint or point,
        x = tonumber(x) or 0,
        y = tonumber(y) or 0,
    }
end

function ns:GetFeatureScale(scaleKey, minScale, maxScale, defaultScale)
    local db = self:EnsureFeatureDB()
    local scale = type(scaleKey) == "string" and scaleKey ~= "" and type(db) == "table" and tonumber(db[scaleKey]) or nil
    if not scale then
        return tonumber(defaultScale) or 1
    end

    return self:ClampNumber(scale, minScale, maxScale)
end

function ns:ApplyFeatureScale(frame, scaleKey, minScale, maxScale, defaultScale)
    if not frame or type(frame.SetScale) ~= "function" then
        return
    end

    frame:SetScale(self:GetFeatureScale(scaleKey, minScale, maxScale, defaultScale))
end

function ns:StoreFeatureScale(scaleKey, scale, minScale, maxScale)
    if type(scaleKey) ~= "string" or scaleKey == "" then
        return
    end

    local db = self:EnsureFeatureDB()
    if type(db) ~= "table" then
        return
    end

    db[scaleKey] = self:ClampNumber(scale, minScale, maxScale)
end

local function ensureFeatureOptionsDB()
    local db = ns:EnsureFeatureDB()
    if type(db) ~= "table" then
        return nil
    end

    db.featureOptions = type(db.featureOptions) == "table" and db.featureOptions or {}
    return db.featureOptions
end



local function resolveFeatureOptionToggle(definition, optionToggle)
    if type(definition) ~= "table" or type(optionToggle) ~= "table" then
        return nil
    end
    local optionKey = optionToggle.key
    if type(optionKey) ~= "string" or optionKey == "" then
        return nil
    end

    local values = {}
    for _, value in ipairs(optionToggle.values or {}) do
        if value ~= nil then
            values[#values + 1] = value
        end
    end
    if #values == 0 then
        return nil
    end

    local currentValue = nil
    if ns.GetFeatureOptionValue then
        local okCurrentValue, resolvedCurrentValue = pcall(ns.GetFeatureOptionValue, ns, definition.id, optionKey)
        if okCurrentValue then
            currentValue = resolvedCurrentValue
        end
    end
    local valueLabels = {}
    for _, value in ipairs(values) do
        local rawLabel = optionToggle.valueLabels and optionToggle.valueLabels[value] or nil
        valueLabels[value] = resolveText(rawLabel) or tostring(value)
    end

    if currentValue == nil then
        currentValue = optionToggle.defaultValue or values[1]
    end

    if type(optionToggle.getValue) == "function" then
        local okDynamic, dynamicValue = pcall(optionToggle.getValue)
        if okDynamic and dynamicValue ~= nil then
            currentValue = dynamicValue
        end
    end

    return {
        key = optionKey,
        label = resolveText(optionToggle.label),
        values = values,
        currentValue = currentValue,
        currentLabel = valueLabels[currentValue] or tostring(currentValue),
        valueLabels = valueLabels,
    }
end

local function findFeatureOptionToggle(definition, optionKey)
    if type(definition) ~= "table" or type(optionKey) ~= "string" or optionKey == "" then
        return nil
    end

    for _, optionToggle in ipairs({
        definition.optionToggle,
        definition.secondaryOptionToggle,
    }) do
        if type(optionToggle) == "table" and optionToggle.key == optionKey then
            return optionToggle
        end
    end

    return nil
end

local function resolveFeatureData(definition)
    if type(definition) ~= "table" then
        return nil
    end

    local okTitle, title = pcall(resolveText, definition.title)
    local okDescription, description = pcall(resolveText, definition.description)
    local okSummary, summary = pcall(resolveText, definition.summary)
    local okNote, note = pcall(resolveText, definition.note)
    local okPreviewLabel, previewLabel = pcall(resolveText, definition.previewLabel)
    local okPreviewNote, previewNote = pcall(resolveText, definition.previewNote)
    local okOptionToggle, optionToggle = pcall(resolveFeatureOptionToggle, definition, definition.optionToggle)
    local okSecondaryOptionToggle, secondaryOptionToggle = pcall(resolveFeatureOptionToggle, definition, definition.secondaryOptionToggle)
    local enabled = false
    if ns.IsFeatureEnabled then
        local okEnabled, resolvedEnabled = pcall(ns.IsFeatureEnabled, ns, definition.id)
        if okEnabled then
            enabled = resolvedEnabled == true
        end
    end

    return {
        id = definition.id,
        order = tonumber(definition.order) or 0,
        category = definition.category,
        title = okTitle and title or tostring(definition.id or ""),
        description = okDescription and description or "",
        summary = okSummary and summary or "",
        note = okNote and note or "",
        previewLabel = okPreviewLabel and previewLabel or "",
        previewNote = okPreviewNote and previewNote or "",
        optionToggle = okOptionToggle and optionToggle or nil,
        secondaryOptionToggle = okSecondaryOptionToggle and secondaryOptionToggle or nil,
        requiresReload = definition.requiresReload == true,
        planned = definition.planned == true,
        enabled = enabled,
    }
end

function ns:GetFeatureCategories()
    local categories = {}
    for _, category in ipairs(FEATURE_CATEGORIES) do
        local categoryFeatures = {}
        local okCategory, resolvedFeatures = pcall(self.GetFeatureDefinitions, self, category.key)
        if okCategory and type(resolvedFeatures) == "table" then
            categoryFeatures = resolvedFeatures
        elseif self.Print then
            self:Print(string.format("Feature category '%s' failed: %s", tostring(category.key), tostring(resolvedFeatures)))
        end

        if #categoryFeatures > 0 then
            categories[#categories + 1] = {
                key = category.key,
                label = resolveText(category.label),
                description = resolveText(category.description),
            }
        end
    end
    return categories
end

function ns:GetFeatureDefinition(featureID)
    local definition = FEATURE_DEFINITIONS[featureID]
    if not definition then
        return nil
    end

    local okResolved, resolvedDefinition = pcall(resolveFeatureData, definition)
    if okResolved then
        return resolvedDefinition
    end

    if self.Print then
        self:Print(string.format("Feature '%s' failed to resolve: %s", tostring(featureID), tostring(resolvedDefinition)))
    end

    return nil
end

function ns:GetFeatureDefinitions(categoryKey)
    local features = {}
    for _, featureID in ipairs(FEATURE_ORDER) do
        local definition = FEATURE_DEFINITIONS[featureID]
        if definition and (categoryKey == nil or definition.category == categoryKey) then
            local okResolved, resolvedDefinition = pcall(resolveFeatureData, definition)
            if okResolved and type(resolvedDefinition) == "table" then
                features[#features + 1] = resolvedDefinition
            elseif self.Print then
                self:Print(string.format("Feature '%s' failed to resolve: %s", tostring(featureID), tostring(resolvedDefinition)))
            end
        end
    end

    table.sort(features, function(a, b)
        if (a.order or 0) ~= (b.order or 0) then
            return (a.order or 0) < (b.order or 0)
        end
        return (a.title or "") < (b.title or "")
    end)

    return features
end

function ns:GetDefaultFeatureCategory()
    for _, category in ipairs(FEATURE_CATEGORIES) do
        if #self:GetFeatureDefinitions(category.key) > 0 then
            return category.key
        end
    end
    return FEATURE_CATEGORIES[1] and FEATURE_CATEGORIES[1].key or nil
end

function ns:IsFeatureEnabled(featureID)
    local definition = FEATURE_DEFINITIONS[featureID]
    if not definition then
        return false
    end
    if definition.planned == true then
        return false
    end

    local db = self:EnsureFeatureDB()
    if type(db) ~= "table" then
        return false
    end

    if db[featureID] == nil then
        return false
    end

    return db[featureID] == true
end

function ns:SetFeatureEnabled(featureID, enabled)
    local definition = FEATURE_DEFINITIONS[featureID]
    if not definition then
        return false
    end
    if definition.planned == true then
        return false
    end

    local db = self:EnsureFeatureDB()
    if type(db) ~= "table" then
        return false
    end

    db[featureID] = enabled == true

    if type(definition.apply) == "function" then
        local ok, err = pcall(definition.apply, db[featureID], "toggle")
        if not ok and ns.Print then
            ns:Print(string.format("Feature '%s' failed to apply: %s", tostring(featureID), tostring(err)))
        end
    end

    if ns.RefreshWorldMapFeatureButton then
        pcall(ns.RefreshWorldMapFeatureButton, ns)
    end

    return db[featureID]
end

function ns:GetFeatureOptionValue(featureID, optionKey)
    local definition = FEATURE_DEFINITIONS[featureID]
    if not definition or type(optionKey) ~= "string" or optionKey == "" then
        return nil
    end

    local optionToggle = findFeatureOptionToggle(definition, optionKey)
    local defaultValue = optionToggle and optionToggle.defaultValue or nil
    local values = optionToggle and optionToggle.values or nil

    local optionsDB = ensureFeatureOptionsDB()
    local featureOptions = type(optionsDB) == "table" and type(optionsDB[featureID]) == "table" and optionsDB[featureID] or nil
    local value = featureOptions and featureOptions[optionKey] or nil
    if value == nil then
        value = defaultValue
    end

    if type(optionToggle.getValue) == "function" then
        local okDynamic, dynamicValue = pcall(optionToggle.getValue)
        if okDynamic and dynamicValue ~= nil then
            value = dynamicValue
        end
    end

    if type(values) == "table" and #values > 0 then
        for _, candidate in ipairs(values) do
            if candidate == value then
                return value
            end
        end
        return values[1]
    end

    return value
end

function ns:SetFeatureOptionValue(featureID, optionKey, value)
    local definition = FEATURE_DEFINITIONS[featureID]
    if not definition or type(optionKey) ~= "string" or optionKey == "" then
        return nil
    end

    local optionToggle = findFeatureOptionToggle(definition, optionKey)
    if not optionToggle then
        return nil
    end

    local allowedValue = nil
    for _, candidate in ipairs(optionToggle.values or {}) do
        if candidate == value then
            allowedValue = candidate
            break
        end
    end
    if allowedValue == nil then
        allowedValue = optionToggle.defaultValue or optionToggle.values[1]
    end

    local optionsDB = ensureFeatureOptionsDB()
    if type(optionsDB) ~= "table" then
        return nil
    end

    optionsDB[featureID] = type(optionsDB[featureID]) == "table" and optionsDB[featureID] or {}
    optionsDB[featureID][optionKey] = allowedValue

    if type(definition.onOptionChanged) == "function" then
        local ok, err = pcall(definition.onOptionChanged, allowedValue, optionKey)
        if not ok and ns.Print then
            ns:Print(string.format("Feature option '%s.%s' failed: %s", tostring(featureID), tostring(optionKey), tostring(err)))
        end
    elseif type(definition.apply) == "function" then
        local ok, err = pcall(definition.apply, ns:IsFeatureEnabled(featureID), "option", optionKey, allowedValue)
        if not ok and ns.Print then
            ns:Print(string.format("Feature '%s' option apply failed: %s", tostring(featureID), tostring(err)))
        end
    end

    return allowedValue
end

function ns:CycleFeatureOption(featureID, optionKey)
    local definition = FEATURE_DEFINITIONS[featureID]
    local optionToggle = findFeatureOptionToggle(definition, optionKey)
    if type(optionToggle) ~= "table" then
        return nil
    end

    local values = optionToggle.values or {}
    if #values == 0 then
        return nil
    end

    local currentValue = self:GetFeatureOptionValue(featureID, optionKey)
    local nextIndex = 1
    for index, candidate in ipairs(values) do
        if candidate == currentValue then
            nextIndex = (index % #values) + 1
            break
        end
    end

    return self:SetFeatureOptionValue(featureID, optionKey, values[nextIndex])
end

function ns:ApplyFeatureStates()
    self:EnsureFeatureDB()

    for _, featureID in ipairs(FEATURE_ORDER) do
        local definition = FEATURE_DEFINITIONS[featureID]
        if definition and type(definition.apply) == "function" then
            local enabled = self:IsFeatureEnabled(featureID)
            local ok, err = pcall(definition.apply, enabled, "load")
            if not ok and self.Print then
                self:Print(string.format("Feature '%s' failed during load: %s", tostring(featureID), tostring(err)))
            end
        end
    end
end
