
local API = require("SN2ModSettings")
local UEHelpers = require("UEHelpers")

local function log(msg)
    print(string.format("[SN2ModSettings] %s\n", msg))
end

local function CreateWidget(widgetClass, worldContext)
    if not widgetClass or not widgetClass:IsValid() then return nil end

    local lib = StaticFindObject("/Script/UMG.Default__WidgetBlueprintLibrary")
    if not lib or not lib:IsValid() then
        log("UWidgetBlueprintLibrary CDO not found")
        return nil
    end

    local owningPlayer = UEHelpers.GetPlayerController()
    if not owningPlayer or not owningPlayer:IsValid() then
        log("No PlayerController for widget Create")
        return nil
    end

    return lib:Create(worldContext, widgetClass, owningPlayer)
end

local function ClassNameOf(obj)
    if not obj or not obj:IsValid() then return "<invalid>" end
    local c = obj:GetClass()
    if not c or not c:IsValid() then return "<no class>" end
    return c:GetFName():ToString()
end

do
    local body = [=[-- Auto-generated by SN2ModSettings. Do not edit; will be regenerated.
return {
    name     = "SN2ModSettings",
    display  = "Mod Settings",
    version  = "1.1.1",
    github   = "Just-Chaldea/SN2-ModSettings",
    nexus_id = "20",
    settings = {
        { key="CheckForUpdates", title="Check for updates",
          description="Periodically check GitHub for new versions of installed mods. Runs once at game start; results cached for 3 hours. No personal data is sent.",
          type="toggle", default=true },
    },
}
]=]
    local path = API.REGISTRATIONS_DIR .. "SN2ModSettings.lua"
    local f = io.open(path, "w")
    if f then
        f:write(body)
        f:close()
        log("Self-manifest written")
    else
        log(string.format("Self-manifest write FAILED to %s", path))
    end
end

local Registrations = {}
local function RefreshRegistrations()
    Registrations = {}
    local list = API.LoadRegistrations()
    for _, m in ipairs(list) do
        Registrations[m.name] = m
    end
end
RefreshRegistrations()
log(string.format("Loaded %d registration(s) at startup", (function()
    local n = 0; for _ in pairs(Registrations) do n = n + 1 end; return n
end)()))

do
    local published_mods = 0
    local published_keys = 0
    for mod_id, manifest in pairs(Registrations) do
        local saved = API.LoadLuaTable(API.SAVED_DIR .. mod_id .. ".lua") or {}
        if ModRef then

            for _, def in ipairs(manifest.settings or {}) do
                local v = saved[def.key]
                if v == nil then v = def.default end
                local vt = type(v)
                if vt == "number" or vt == "boolean" or vt == "string" then
                    pcall(function()
                        ModRef:SetSharedVariable("SN2ModSettings/" .. mod_id .. "/" .. def.key, v)
                    end)
                    published_keys = published_keys + 1
                end
            end
            published_mods = published_mods + 1
        end
    end
    log(string.format("Published %d primitive(s) across %d mod(s) to SharedVariable at startup",
        published_keys, published_mods))
end

local InjectedScreens   = {}
local SettingInstances  = {}

local TabButtons        = {}
local SidePanelRefs     = nil

local UpdateInfo = {}

local NexusButtonUrls = {}

local KeybindWidgetMap = {}

local function CheckForUpdatesEnabled()
    local v = API.GetValue("SN2ModSettings", "CheckForUpdates", true)
    if type(v) == "boolean" then return v end
    return true
end

local function ComposeSectionTitle(mod_id, manifest)
    if not manifest then return mod_id end
    local base = manifest.display or manifest.name or mod_id
    local version = manifest.version
    if type(version) ~= "string" or version == "" then return base end

    if CheckForUpdatesEnabled() then
        local info = UpdateInfo[mod_id]
        if info and type(info.latest) == "string" and info.latest ~= ""
           and API.CompareVersions(info.latest, version) > 0 then
            return string.format("%s - v%s - Update Available! v%s",
                base, version, info.latest)
        end
    end
    return string.format("%s - v%s", base, version)
end

local InjectionQueue    = {}

local ApplyEnabledStates

local function InjectModsTab(screen)
    local addr = tostring(screen:GetAddress())
    if InjectedScreens[addr] then return end

    local accTab = screen.WBP_TabAccessibilitySettings
    local audioSel = screen.AudioSelector
    local switcher = screen.Contents
    local tabHBox  = screen.TabSelectors
    local btnGroup = screen.TabButtonGroup
    local tabMapping = screen.TabMapping

    if not (accTab and accTab:IsValid()
        and audioSel and audioSel:IsValid()
        and switcher and switcher:IsValid()
        and tabHBox and tabHBox:IsValid()
        and btnGroup and btnGroup:IsValid())
    then
        return
    end

    InjectedScreens[addr] = true
    log(string.format("Found Settings2Screen %s - injecting Mods tab", screen:GetFullName()))

    local function FindModsTabIndex()
        local found = nil
        pcall(function()
            tabMapping:ForEach(function(idx, elem)
                local fnameOk, fname = pcall(function() return elem:Get() end)
                if not (fnameOk and fname) then return end
                local strOk, str = pcall(function() return fname:ToString() end)
                if strOk and str == "Mods" then
                    found = idx
                    return true
                end
            end)
        end)
        return found
    end

    local removedOrphans = 0
    for _ = 1, 16 do
        local orphanIdx = FindModsTabIndex()
        if not orphanIdx then break end

        local orphanBtn, orphanTab
        pcall(function() orphanBtn = tabHBox:GetChildAt(orphanIdx - 1) end)
        pcall(function() orphanTab = switcher:GetChildAt(orphanIdx - 1) end)

        if orphanBtn and orphanBtn:IsValid() then
            pcall(function() btnGroup:RemoveButton(orphanBtn) end)
        end

        pcall(function() switcher:RemoveChildAt(orphanIdx - 1) end)
        pcall(function() tabHBox:RemoveChildAt(orphanIdx - 1) end)

        local snapshot = {}
        pcall(function()
            tabMapping:ForEach(function(_, elem)
                local fname
                pcall(function() fname = elem:Get() end)
                if fname then
                    local str
                    pcall(function() str = fname:ToString() end)
                    if str then snapshot[#snapshot + 1] = str end
                end
            end)
        end)
        pcall(function() tabMapping:Empty() end)
        local skipped = false
        for _, name in ipairs(snapshot) do
            if not skipped and name == "Mods" then
                skipped = true
            else
                pcall(function() tabMapping[#tabMapping + 1] = FName(name) end)
            end
        end

        removedOrphans = removedOrphans + 1
    end
    if removedOrphans > 0 then
        log(string.format("Cleaned %d orphan Mods tab(s) from prior Lua state(s) (hot-reload)", removedOrphans))
    end

    local TabClass = accTab:GetClass()
    local BtnClass = audioSel:GetClass()

    local newTab = CreateWidget(TabClass, screen)
    if not (newTab and newTab:IsValid()) then
        log("FAILED to Create tab content widget")
        return
    end
    log(string.format("Created tab content: %s", newTab:GetFullName()))

    local newBtn = CreateWidget(BtnClass, screen)
    if not (newBtn and newBtn:IsValid()) then
        log("FAILED to Create tab button")
        return
    end
    log(string.format("Created tab button: %s", newBtn:GetFullName()))

    switcher:AddChild(newTab)

    tabHBox:AddChild(newBtn)

    btnGroup:AddWidget(newBtn)

    local modsTabName = FName("Mods")
    tabMapping[#tabMapping + 1] = modsTabName

    log("Mods tab added to screen")

    SidePanelRefs = {
        screen     = screen,
        titleBlock = screen.SettingTitle,
        descBlock  = screen.SidePanel_CommonTextBlock,

        sideBox    = screen.VerticalBox_SettingsSidePanel,
    }
    log(string.format("Side panel refs: title=%s desc=%s sideBox=%s",
        SidePanelRefs.titleBlock and "valid" or "nil",
        SidePanelRefs.descBlock and "valid" or "nil",
        SidePanelRefs.sideBox and "valid" or "nil"))

    local textBlock = newBtn.TextBlock_Tab
    if textBlock and textBlock:IsValid() then
        pcall(function() textBlock:SetText(FText("Mods")) end)
        TabButtons[tostring(newBtn:GetAddress())] = { button = newBtn, label = "Mods" }
    end

    local grid = newTab.UWESimpleGrid
    if not grid or not grid:IsValid() then
        log("New tab has no UWESimpleGrid yet - slider injection deferred")
        return
    end

    local cleared, clearErr = pcall(function() grid:ClearChildren() end)
    if cleared then
        log("Cleared inherited widgets from new tab's grid")
    else
        log(string.format("ClearChildren failed (continuing): %s", tostring(clearErr)))
    end

    local sliderTemplate = accTab.ApplicationScaleSlider
    if not sliderTemplate or not sliderTemplate:IsValid() then
        log("No slider template available")
        return
    end
    local SliderClass = sliderTemplate:GetClass()

    local toggleTemplate = accTab.TextScaleToggle
    local ToggleClass = (toggleTemplate and toggleTemplate:IsValid()) and toggleTemplate:GetClass() or nil
    if not ToggleClass then log("No toggle template available (toggles will be skipped)") end

    local genericTemplate
    pcall(function() genericTemplate = accTab.WBP_SettingsButton.WBP_GenericButtonBig end)
    local GenericButtonClass = (genericTemplate and genericTemplate:IsValid())
        and genericTemplate:GetClass() or nil
    if not GenericButtonClass then
        log("No GenericButtonBig template available (Nexus buttons will be skipped)")
    end

    local KeybindRowClass = StaticFindObject(
        "/Game/Blueprints/UI/Settings2/WBP_SettingsKeybind.WBP_SettingsKeybind_C")
    if KeybindRowClass and KeybindRowClass:IsValid() then
        log(string.format("Keybind row class captured: %s",
            KeybindRowClass:GetFullName()))
    else
        KeybindRowClass = nil
        log("No WBP_SettingsKeybind class available (keybinds will be skipped)")
    end

    local OurSettingAddrs = {}

    local currentGrid = grid

    local GridClass = grid:GetClass()
    local origNumColumns  = 1
    local origNumRows     = 0
    local origSlotPadding = nil
    local origSlotMinHeight = 0
    pcall(function() origNumColumns    = grid.NumColumns end)
    pcall(function() origNumRows       = grid.NumRows end)
    pcall(function() origSlotPadding   = grid.SlotPadding end)
    pcall(function() origSlotMinHeight = grid.SlotMinHeight end)
    log(string.format("Grid class=%s NumColumns=%d NumRows=%d SlotMinHeight=%s",
        ClassNameOf(grid), origNumColumns, origNumRows, tostring(origSlotMinHeight)))

    local gridParent = nil
    pcall(function() gridParent = grid:GetParent() end)
    if gridParent and gridParent:IsValid() then
        log(string.format("Grid parent class=%s", ClassNameOf(gridParent)))
    else
        log("Grid has no resolvable parent; falling back to single-grid layout")
        gridParent = nil
    end

    local HeaderWrapperTemplate = nil
    local HeaderTemplate = nil
    do
        local function FindCommonTextBlock(widget, depth, results)
            if depth > 12 or not widget or not widget:IsValid() then return end
            if OurSettingAddrs[widget:GetAddress()] then return end
            if ClassNameOf(widget) == "CommonTextBlock" then
                results[#results + 1] = widget
            end
            local ok, count = pcall(function() return widget:GetChildrenCount() end)
            if ok and count and count > 0 then
                for i = 0, count - 1 do
                    local ok2, child = pcall(function() return widget:GetChildAt(i) end)
                    if ok2 and child and child:IsValid() then
                        FindCommonTextBlock(child, depth + 1, results)
                    end
                end
            end
        end

        local searchRoot = grid
        for _ = 1, 12 do
            local ok, p = pcall(function() return searchRoot:GetParent() end)
            if not ok or not p or not p:IsValid() then break end
            if p == newTab then break end
            searchRoot = p
        end
        local found = {}
        pcall(function() FindCommonTextBlock(searchRoot, 0, found) end)
        if #found > 0 then
            HeaderTemplate = found[1]
            log(string.format("Header template captured (class=%s)",
                ClassNameOf(HeaderTemplate)))
        else
            log("No CommonTextBlock found to use as header template")
        end
    end

    local function InjectSlider(mod_id, def, targetGrid)
        local current = API.GetValue(mod_id, def.key, def.default)
        local slider = CreateWidget(SliderClass, newTab)
        if not (slider and slider:IsValid()) then return false end
        OurSettingAddrs[slider:GetAddress()] = true

        if not (targetGrid and targetGrid:IsValid()) then
            log(string.format("InjectSlider %s/%s: targetGrid invalid (screen torn down mid-drain?)", mod_id, def.key))
            return false
        end
        targetGrid:AddChild(slider)

        local format = def.format or "float"
        local display_as_percent = (format == "percent")
        local step = def.step or ((format == "integer") and 1.0 or 0.1)

        slider["Min Value"] = def.min
        slider["Max Value"] = def.max
        slider["Step Size (Rounding)"] = step
        slider.Value = current
        slider.DisplayMinAndMaxValues = true
        pcall(function() slider["Display As Percentage"] = display_as_percent end)

        local innerSlider = slider.WBP_Slider
        if innerSlider and innerSlider:IsValid() then
            pcall(function() innerSlider["Display As Percentage"] = display_as_percent end)
            pcall(function() innerSlider:UpdateSliderParameters(display_as_percent, step) end)
            pcall(function() innerSlider:SetMinValue(def.min, true) end)
            pcall(function() innerSlider:SetMaxValue(def.max, true) end)
            pcall(function() innerSlider:SetValue(current) end)
        end

        local label = slider.WBP_SettingsLabel
        if label and label:IsValid() then
            pcall(function() label:SetTitle(FText(def.title or def.key)) end)
        end

        SettingInstances[tostring(slider:GetAddress())] = {
            widget = slider, kind = "slider", mod_id = mod_id, key = def.key,
            last_value = current,
            title = def.title or def.key,
            description = def.description or "",
            enabled_by = def.enabled_by,
        }
        log(string.format("  + slider %s/%s (default=%s, range %.2f..%.2f%s)",
            mod_id, def.key, tostring(current), def.min, def.max,
            def.enabled_by and (", gated by " .. def.enabled_by) or ""))
        return true
    end

    local function InjectToggle(mod_id, def, targetGrid)
        if not ToggleClass then return false end
        local default = def.default
        if default == nil then default = false end
        local current = API.GetValue(mod_id, def.key, default)

        if type(current) ~= "boolean" then current = (current and current ~= 0) and true or false end

        local toggle = CreateWidget(ToggleClass, newTab)
        if not (toggle and toggle:IsValid()) then return false end
        OurSettingAddrs[toggle:GetAddress()] = true

        if not (targetGrid and targetGrid:IsValid()) then
            log(string.format("InjectToggle %s/%s: targetGrid invalid (screen torn down mid-drain?)", mod_id, def.key))
            return false
        end
        targetGrid:AddChild(toggle)

        pcall(function() toggle:SetValue(current) end)
        pcall(function() toggle.Value = current end)

        local label = toggle.WBP_SettingsLabel
        if label and label:IsValid() then
            pcall(function() label:SetTitle(FText(def.title or def.key)) end)
        end

        SettingInstances[tostring(toggle:GetAddress())] = {
            widget = toggle, kind = "toggle", mod_id = mod_id, key = def.key,
            last_value = current,
            title = def.title or def.key,
            description = def.description or "",
            enabled_by = def.enabled_by,
        }
        log(string.format("  + toggle %s/%s (default=%s%s)",
            mod_id, def.key, tostring(current),
            def.enabled_by and (", gated by " .. def.enabled_by) or ""))
        return true
    end

    local function InjectButton(mod_id, manifest, targetGrid)
        if not GenericButtonClass then return false end
        local nexus_id = manifest.nexus_id
        if type(nexus_id) ~= "string" or nexus_id == "" then return false end

        local button = CreateWidget(GenericButtonClass, newTab)
        if not (button and button:IsValid()) then return false end
        OurSettingAddrs[button:GetAddress()] = true

        if not (targetGrid and targetGrid:IsValid()) then
            log(string.format("InjectButton %s: targetGrid invalid (screen torn down mid-drain?)", mod_id))
            return false
        end

        pcall(function() button:SetText(FText("View on Nexus Mods")) end)

        targetGrid:AddChild(button)

        local url = "https://www.nexusmods.com/subnautica2/mods/" .. nexus_id
        NexusButtonUrls[tostring(button:GetAddress())] = url

        SettingInstances[tostring(button:GetAddress())] = {
            widget = button, kind = "button", mod_id = mod_id, key = "_nexus_button_",
            title = "View on Nexus Mods",
            description = "Opens the mod's page in your default browser:\n" .. url,
        }
        log(string.format("  + button %s/_nexus_button_ -> %s", mod_id, url))
        return true
    end

    local function InjectKeybind(mod_id, def, targetGrid)
        if not KeybindRowClass then return false end

        local row = CreateWidget(KeybindRowClass, newTab)
        if not (row and row:IsValid()) then
            log(string.format("InjectKeybind %s/%s: CreateWidget failed", mod_id, def.key))
            return false
        end
        OurSettingAddrs[row:GetAddress()] = true

        if not (targetGrid and targetGrid:IsValid()) then
            log(string.format("InjectKeybind %s/%s: targetGrid invalid", mod_id, def.key))
            return false
        end
        targetGrid:AddChild(row)

        local label = row.WBP_SettingsLabel
        if label and label:IsValid() then
            local innerTitle = label.Title
            if innerTitle and innerTitle:IsValid() then
                pcall(function() innerTitle:SetText(FText(def.title or def.key)) end)
            end
        end

        local firstSel = row.UWEInputKeySelector_First
        local secondSel = row.UWEInputKeySelector_Second

        local current = API.GetValue(mod_id, def.key, def.default or "")
        local current_alt = API.GetValue(mod_id, def.key .. "_Alt", "")

        local function SetSelectorPlaceholder(sel, key_name)
            if not (sel and sel:IsValid()) then return end
            if type(key_name) ~= "string" or key_name == "" then return end
            pcall(function() sel:SetNoKeySpecifiedText(FText(key_name)) end)
        end
        SetSelectorPlaceholder(firstSel, current)
        SetSelectorPlaceholder(secondSel, current_alt)

        if firstSel and firstSel:IsValid() then
            KeybindWidgetMap[tostring(firstSel:GetAddress())] = {
                mod_id     = mod_id,
                key        = def.key,
                widget     = firstSel,
                last_value = current,
            }
        end
        if secondSel and secondSel:IsValid() then
            KeybindWidgetMap[tostring(secondSel:GetAddress())] = {
                mod_id     = mod_id,
                key        = def.key .. "_Alt",
                widget     = secondSel,
                last_value = current_alt,
            }
        end

        SettingInstances[tostring(row:GetAddress())] = {
            widget = row, kind = "keybind", mod_id = mod_id, key = def.key,
            title = def.title or def.key,
            description = def.description or "",
        }
        log(string.format("  + keybind %s/%s (primary=%s, alt=%s)",
            mod_id, def.key, tostring(current),
            (current_alt == "") and "<unset>" or tostring(current_alt)))
        return true
    end

    local function DeepCloneWidget(src, outer)
        if not src or not src:IsValid() then return nil end
        local cls = src:GetClass()
        if not cls or not cls:IsValid() then return nil end
        local clone = StaticConstructObject(cls, outer)
        if not (clone and clone:IsValid()) then return nil end
        OurSettingAddrs[clone:GetAddress()] = true

        local cname = ClassNameOf(src)
        if cname == "Image" then
            pcall(function() clone.Brush           = src.Brush end)
            pcall(function() clone.ColorAndOpacity = src.ColorAndOpacity end)
        elseif cname == "CommonTextBlock" then
            pcall(function() clone.Style = src.Style end)
        elseif cname == "SizeBox" then

            local function copyIfSet(valueName, setterName)
                local v
                pcall(function() v = src[valueName] end)
                if type(v) == "number" and v > 0 then
                    pcall(function() clone[setterName](clone, v) end)
                end
            end
            copyIfSet("WidthOverride",     "SetWidthOverride")
            copyIfSet("HeightOverride",    "SetHeightOverride")
            copyIfSet("MinDesiredWidth",   "SetMinDesiredWidth")
            copyIfSet("MinDesiredHeight",  "SetMinDesiredHeight")
            copyIfSet("MaxDesiredWidth",   "SetMaxDesiredWidth")
            copyIfSet("MaxDesiredHeight",  "SetMaxDesiredHeight")
        end

        local cc
        pcall(function() cc = src:GetChildrenCount() end)
        if cc and cc > 0 then
            for i = 0, cc - 1 do
                local srcChild
                pcall(function() srcChild = src:GetChildAt(i) end)
                if srcChild and srcChild:IsValid() then
                    local dstChild = DeepCloneWidget(srcChild, outer)
                    if dstChild then
                        pcall(function() clone:AddChild(dstChild) end)
                        local srcSlot, dstSlot
                        pcall(function() srcSlot = srcChild.Slot end)
                        pcall(function() dstSlot = dstChild.Slot end)
                        if srcSlot and srcSlot:IsValid() and dstSlot and dstSlot:IsValid() then
                            pcall(function() dstSlot.Padding             = srcSlot.Padding end)
                            pcall(function() dstSlot.HorizontalAlignment = srcSlot.HorizontalAlignment end)
                            pcall(function() dstSlot.VerticalAlignment   = srcSlot.VerticalAlignment end)
                            pcall(function() dstSlot.Size                = srcSlot.Size end)
                        end
                    end
                end
            end
        end

        return clone
    end

    local function FindCommonTextBlockIn(root, depth)
        if not root or not root:IsValid() then return nil end
        depth = depth or 0
        if depth > 6 then return nil end
        if ClassNameOf(root) == "CommonTextBlock" then return root end
        local cc
        pcall(function() cc = root:GetChildrenCount() end)
        if cc and cc > 0 then
            for i = 0, cc - 1 do
                local ch
                pcall(function() ch = root:GetChildAt(i) end)
                local found = FindCommonTextBlockIn(ch, depth + 1)
                if found then return found end
            end
        end
        return nil
    end

    local function MakeHeaderWidget(header_mod_id, manifest)
        if not HeaderWrapperTemplate or not HeaderWrapperTemplate:IsValid() then
            log("MakeHeaderWidget: no wrapper template")
            return nil
        end
        local hdr = DeepCloneWidget(HeaderWrapperTemplate, newTab)
        if not (hdr and hdr:IsValid()) then
            log("MakeHeaderWidget: DeepCloneWidget failed")
            return nil
        end
        local initialTitle = ComposeSectionTitle(header_mod_id, manifest)
        local innerText = FindCommonTextBlockIn(hdr, 0)
        if innerText and innerText:IsValid() then
            pcall(function() innerText:SetText(FText(initialTitle)) end)
        else
            log("MakeHeaderWidget: cloned wrapper has no CommonTextBlock; header text will be empty")
        end
        SettingInstances[tostring(hdr:GetAddress())] = {
            widget = hdr, kind = "header",

            mod_id = header_mod_id,
            key = header_mod_id,
            last_value = nil,
            title = initialTitle,
            description = "",

            textWidget = innerText,
        }
        return hdr
    end

    local function MakeGridWidget()
        local g = StaticConstructObject(GridClass, newTab)
        if not (g and g:IsValid()) then
            log(string.format("MakeGridWidget: StaticConstructObject(%s) failed",
                ClassNameOf(GridClass)))
            return nil
        end
        OurSettingAddrs[g:GetAddress()] = true

        pcall(function() g.NumColumns    = origNumColumns end)
        pcall(function() g.NumRows       = origNumRows end)
        if origSlotPadding then
            pcall(function() g.SlotPadding = origSlotPadding end)
        end
        pcall(function() g.SlotMinHeight = origSlotMinHeight end)
        return g
    end

    local InheritedHeaders = {}
    do
        local function FindLabelsInTree(widget, depth, results)
            if depth > 12 or not widget or not widget:IsValid() then return end
            if OurSettingAddrs[widget:GetAddress()] then return end
            if ClassNameOf(widget) == "CommonTextBlock" then
                results[#results + 1] = widget
            end
            local ok, count = pcall(function() return widget:GetChildrenCount() end)
            if ok and count and count > 0 then
                for i = 0, count - 1 do
                    local ok2, child = pcall(function() return widget:GetChildAt(i) end)
                    if ok2 and child and child:IsValid() then
                        FindLabelsInTree(child, depth + 1, results)
                    end
                end
            end
        end

        local searchRoot = grid
        for _ = 1, 12 do
            local ok, p = pcall(function() return searchRoot:GetParent() end)
            if not ok or not p or not p:IsValid() then break end
            if p == newTab then break end
            searchRoot = p
        end
        pcall(function() FindLabelsInTree(searchRoot, 0, InheritedHeaders) end)
        log(string.format("Found %d inherited section-header(s) within newTab subtree", #InheritedHeaders))
    end

    if InheritedHeaders[1] and InheritedHeaders[1]:IsValid() then
        local parent
        pcall(function() parent = InheritedHeaders[1]:GetParent() end)
        if parent and parent:IsValid() then
            HeaderWrapperTemplate = parent
            log(string.format("Header wrapper template captured: class=%s name=%s",
                ClassNameOf(parent), parent:GetFName():ToString()))
        end
    end

    local sortedMods = {}
    for mod_id, manifest in pairs(Registrations) do
        sortedMods[#sortedMods + 1] = { id = mod_id, manifest = manifest }
    end

    table.sort(sortedMods, function(a, b)
        if a.id == "SN2ModSettings" then return true end
        if b.id == "SN2ModSettings" then return false end
        return (a.manifest.display or a.manifest.name or a.id)
             < (b.manifest.display or b.manifest.name or b.id)
    end)

    local queuedCount = 0
    for idx, entry in ipairs(sortedMods) do
        local mod_id   = entry.id
        local manifest = entry.manifest
        local initialTitle = ComposeSectionTitle(mod_id, manifest)

        if idx == 1 then

            for _, hdrInTree in ipairs(InheritedHeaders) do
                pcall(function() hdrInTree:SetText(FText(initialTitle)) end)
                if hdrInTree:IsValid() then
                    SettingInstances[tostring(hdrInTree:GetAddress())] = {
                        widget = hdrInTree, kind = "header",
                        mod_id = mod_id,
                        key = mod_id,
                        last_value = nil,
                        title = initialTitle,
                        description = "",
                        textWidget = hdrInTree,
                    }
                end
            end
            currentGrid = grid
        else

            if gridParent then
                local hdr = MakeHeaderWidget(mod_id, manifest)
                if hdr then pcall(function() gridParent:AddChild(hdr) end) end

                local newGrid = MakeGridWidget()
                if newGrid then
                    pcall(function() gridParent:AddChild(newGrid) end)
                    currentGrid = newGrid
                else
                    currentGrid = grid
                end
            else
                currentGrid = grid
            end
        end

        for _, def in ipairs(manifest.settings or {}) do
            local handler
            if def.type == "slider" then handler = InjectSlider
            elseif def.type == "toggle" then handler = InjectToggle
            elseif def.type == "keybind" then handler = InjectKeybind
            end
            if handler then

                local boundHandler = handler
                local boundModId   = mod_id
                local boundDef     = def
                local boundGrid    = currentGrid
                table.insert(InjectionQueue, function()
                    local ok, err = pcall(boundHandler, boundModId, boundDef, boundGrid)
                    if not ok then
                        log(string.format("Setting %s/%s (type=%s) FAILED: %s",
                            boundModId, boundDef.key, boundDef.type, tostring(err)))
                    end
                end)
                queuedCount = queuedCount + 1
            else
                log(string.format("Unknown setting type %q for %s/%s, skipping",
                    tostring(def.type), mod_id, def.key))
            end
        end

        if GenericButtonClass and type(manifest.nexus_id) == "string" and manifest.nexus_id ~= "" then
            local boundModId   = mod_id
            local boundManifest = manifest
            local boundGrid    = currentGrid
            table.insert(InjectionQueue, function()
                local ok, err = pcall(InjectButton, boundModId, boundManifest, boundGrid)
                if not ok then
                    log(string.format("Nexus button %s FAILED: %s", boundModId, tostring(err)))
                end
            end)
            queuedCount = queuedCount + 1
        end
    end
    log(string.format("Queued %d injection(s) across %d mod(s) for chunked drain",
        queuedCount, #sortedMods))

end

local function SafeInjectModsTab(screen)
    local ok, err = pcall(InjectModsTab, screen)
    if not ok then
        log(string.format("InjectModsTab errored: %s", tostring(err)))
    end
end

ApplyEnabledStates = function()
    local byKey = {}
    for _, info in pairs(SettingInstances) do
        byKey[info.mod_id .. "|" .. info.key] = info
    end
    for _, dep in pairs(SettingInstances) do
        if dep.enabled_by then
            local ctrl = byKey[dep.mod_id .. "|" .. dep.enabled_by]
            if ctrl and ctrl.widget and ctrl.widget:IsValid()
               and dep.widget and dep.widget:IsValid() then
                local v = ctrl.widget.Value
                local enabled
                if type(v) == "boolean" then
                    enabled = v
                elseif type(v) == "number" then
                    enabled = v ~= 0
                else
                    enabled = v and true or false
                end
                pcall(function() dep.widget:SetIsEnabled(enabled) end)
            end
        end
    end
end

local SIDE_PANEL_HOOK_PATHS = {
    "/Game/Blueprints/UI/Settings2/WBP_SettingsSlider.WBP_SettingsSlider_C:ShowHighlight",
    "/Game/Blueprints/UI/Settings2/WBP_SettingsToggle.WBP_SettingsToggle_C:ShowHighlight",

    "/Game/Blueprints/UI/GenericUIElements/GenericUI_WBP/WBP_GenericButtonBig.WBP_GenericButtonBig_C:BP_OnHovered",
}
local SidePanelHookedPaths = {}

local NEXUS_BUTTON_CLICK_PATHS = {
    "/Script/CommonUI.CommonButtonBase:HandleButtonClicked",
}
local NexusButtonClickHookedPaths = {}

local function SidePanelHookCallback(SelfCtx)
    local hookOk, hookErr = pcall(function()
        local widget = SelfCtx:get()
        if not widget or not widget:IsValid() then return end

        local info = SettingInstances[tostring(widget:GetAddress())]
        if not info then return end

        if not SidePanelRefs then return end
        local titleBlock = SidePanelRefs.titleBlock
        local descBlock  = SidePanelRefs.descBlock

        if titleBlock and titleBlock:IsValid() then
            pcall(function() titleBlock:SetText(FText(info.title)) end)
        end
        if descBlock and descBlock:IsValid() then
            pcall(function() descBlock:SetText(FText(info.description)) end)
        end

        local sideBox = SidePanelRefs.sideBox
        if sideBox and sideBox:IsValid() then
            pcall(function() sideBox:SetVisibility(4) end)
        end

        log(string.format("Side panel -> %s/%s (%s ShowHighlight)",
            info.mod_id, info.key, info.kind))
    end)
    if not hookOk then log(string.format("ShowHighlight hook callback errored: %s", tostring(hookErr))) end
end

local function TryHookSidePanelDriver()
    for _, path in ipairs(SIDE_PANEL_HOOK_PATHS) do
        if not SidePanelHookedPaths[path] then
            local ok = pcall(function() RegisterHook(path, SidePanelHookCallback) end)
            if ok then
                SidePanelHookedPaths[path] = true
                log(string.format("Registered ShowHighlight hook: %s", path))
            end
        end
    end
end

local function NexusButtonClickCallback(SelfCtx)
    local ok, err = pcall(function()
        local widget = SelfCtx:get()
        if not widget or not widget:IsValid() then return end
        local url = NexusButtonUrls[tostring(widget:GetAddress())]
        if not url then return end
        log(string.format("Nexus button clicked -> %s", url))
        local kismetCDO = StaticFindObject("/Script/Engine.Default__KismetSystemLibrary")
        if kismetCDO and kismetCDO:IsValid() then
            pcall(function() kismetCDO:LaunchURL(url) end)
        else
            log("Nexus button click: KismetSystemLibrary CDO not found")
        end
    end)
    if not ok then log(string.format("Nexus click hook errored: %s", tostring(err))) end
end

local function TryHookNexusButtonClick()
    for _, path in ipairs(NEXUS_BUTTON_CLICK_PATHS) do
        if not NexusButtonClickHookedPaths[path] then
            local ok, err = pcall(function() RegisterHook(path, NexusButtonClickCallback) end)
            if ok then
                NexusButtonClickHookedPaths[path] = true
                log(string.format("Registered click hook: %s", path))
            else
                log(string.format("Failed to register click hook at %s: %s",
                    path, tostring(err)))
            end
        end
    end
end

local SettingsScreenOpen = false

local function ClearWidgetRefs(reason)
    if not SettingsScreenOpen and not SidePanelRefs and next(SettingInstances) == nil
       and #InjectionQueue == 0 then
        return
    end
    SettingsScreenOpen = false
    SettingInstances = {}
    TabButtons = {}
    SidePanelRefs = nil

    NexusButtonUrls = {}
    KeybindWidgetMap = {}

    if #InjectionQueue > 0 then
        log(string.format("Discarding %d pending injection task(s) (screen closed)", #InjectionQueue))
        InjectionQueue = {}
    end
    log(string.format("Settings screen closed (%s) - cleared widget refs", reason))
end

local function IsScreenAlive()
    if not SidePanelRefs then return false end
    if not SidePanelRefs.screen or not SidePanelRefs.screen:IsValid() then return false end

    local ok, outer = pcall(function() return SidePanelRefs.screen:GetOuter() end)
    if not ok or not outer or not outer:IsValid() then return false end
    return true
end

LoopInGameThreadWithDelay(1000, function()
    local screens = FindAllOf("WBP_Settings2Screen_C")
    local anyActive = false
    if type(screens) == "table" then
        for i = 1, #screens do
            local s = screens[i]
            if s and s:IsValid() and not s:GetFName():ToString():find("^Default__") then
                anyActive = true
                SafeInjectModsTab(s)
                TryHookSidePanelDriver()
                TryHookNexusButtonClick()
            end
        end
    end

    if anyActive and not SettingsScreenOpen then
        SettingsScreenOpen = true
    elseif not anyActive and SettingsScreenOpen then
        ClearWidgetRefs("detection loop")
    end
end)

LoopInGameThreadWithDelay(1000, function()
    if not SettingsScreenOpen then return end
    if not IsScreenAlive() then
        ClearWidgetRefs("title-reapply inline check")
        return
    end
    for _, info in pairs(SettingInstances) do
        if info.widget and info.widget:IsValid() then
            if info.kind == "header" then

                local tw = info.textWidget or info.widget
                if tw and tw:IsValid() then
                    local newTitle = ComposeSectionTitle(info.mod_id, Registrations[info.mod_id])
                    if newTitle ~= info.last_pushed_title then
                        info.last_pushed_title = newTitle
                        info.title = newTitle
                        pcall(function() tw:SetText(FText(newTitle)) end)
                    end
                end
            else

                local label = info.widget.WBP_SettingsLabel
                if label and label:IsValid() then

                    local innerTitle = label.Title
                    if innerTitle and innerTitle:IsValid() then
                        pcall(function() innerTitle:SetText(FText(info.title)) end)
                    end
                end
            end
        end
    end

    for _, info in pairs(TabButtons) do
        if info.button and info.button:IsValid() then
            local textBlock = info.button.TextBlock_Tab
            if textBlock and textBlock:IsValid() then
                pcall(function() textBlock:SetText(FText(info.label)) end)
            end
        end
    end
end)

local SuspiciousLogged = {}
LoopInGameThreadWithDelay(200, function()
    if not SettingsScreenOpen then return end
    if not IsScreenAlive() then
        ClearWidgetRefs("value-poll inline check")
        return
    end
    local toDelete = {}
    local anyChanged = false
    for addr, info in pairs(SettingInstances) do
        if not info.widget:IsValid() then
            toDelete[#toDelete + 1] = addr
        elseif info.kind == "slider" or info.kind == "toggle" then

            local v = info.widget.Value
            local vtype = type(v)
            if vtype ~= "number" and vtype ~= "boolean" then

                local key = info.mod_id .. "|" .. info.key
                if not SuspiciousLogged[key] then
                    SuspiciousLogged[key] = true
                    log(string.format("Skipping non-primitive read on %s/%s (type=%s) - widget transiently invalid",
                        info.mod_id, info.key, vtype))
                end
            elseif v ~= info.last_value then
                info.last_value = v
                API.SetValue(info.mod_id, info.key, v)
                log(string.format("%s changed: %s/%s = %s",
                    info.kind, info.mod_id, info.key, tostring(v)))
                anyChanged = true
            end
        end
    end
    for _, addr in ipairs(toDelete) do SettingInstances[addr] = nil end

    local kbToDelete = {}
    for addr, entry in pairs(KeybindWidgetMap) do
        if not (entry.widget and entry.widget:IsValid()) then
            kbToDelete[#kbToDelete + 1] = addr
        else
            local ok, key_name = pcall(function()
                return entry.widget.SelectedKey.Key.KeyName:ToString()
            end)
            if not ok then
                if not entry.read_err_logged then
                    entry.read_err_logged = true
                    log(string.format("keybind read err %s/%s: %s",
                        entry.mod_id, entry.key, tostring(key_name)))
                end
            elseif type(key_name) == "string" then
                if key_name == "None" then key_name = "" end
                if key_name ~= entry.last_value and key_name ~= "" then
                    entry.last_value = key_name
                    API.SetValue(entry.mod_id, entry.key, key_name)
                    log(string.format("keybind changed: %s/%s = %s",
                        entry.mod_id, entry.key, key_name))

                    pcall(function()
                        entry.widget:SetNoKeySpecifiedText(FText(key_name))
                    end)
                    anyChanged = true
                end
            end
        end
    end
    for _, addr in ipairs(kbToDelete) do KeybindWidgetMap[addr] = nil end

    if anyChanged then ApplyEnabledStates() end
end)

local DrainHadWorkLastTick = false
LoopInGameThreadWithDelay(16, function()
    if #InjectionQueue == 0 then
        if DrainHadWorkLastTick then
            DrainHadWorkLastTick = false
            if next(SettingInstances) ~= nil then
                pcall(ApplyEnabledStates)
                log("Chunked injection complete")
            end
        end
        return
    end

    if not SettingsScreenOpen then
        local stale = #InjectionQueue
        InjectionQueue = {}
        DrainHadWorkLastTick = false
        log(string.format("Drain loop: flushed %d stale item(s) (screen not open)", stale))
        return
    end
    local item = table.remove(InjectionQueue, 1)
    DrainHadWorkLastTick = true
    local ok, err = pcall(item)
    if not ok then
        log(string.format("Drain item errored: %s", tostring(err)))
    end
end)

local UPDATE_TRAMPOLINE_PATH = "/Script/Engine.PlayerController:ServerExec"

local UpdateFetchQueue      = {}
local UpdateInFlight        = nil
local UpdateInFlightStartedAt = nil
local UPDATE_FETCH_TIMEOUT_SECONDS = 30
local UpdateHookInstalled   = false
local UpdateTrampolinePC    = nil
local UpdateInitDone        = false

local function NowSeconds() return os.time() end

local function HydrateCache()
    local cache = API.LoadLuaTable(API.UPDATE_CACHE_PATH)
    if type(cache) ~= "table" then return 0 end
    local n = 0
    for mod_id, entry in pairs(cache) do
        if type(entry) == "table" then
            UpdateInfo[mod_id] = entry
            n = n + 1
        end
    end
    return n
end

local function SaveCache()
    local tmp = API.UPDATE_CACHE_PATH .. ".tmp"
    local f = io.open(tmp, "w")
    if not f then return false end
    f:write("-- Generated by SN2ModSettings. Do not edit.\nreturn {\n")
    local keys = {}
    for k in pairs(UpdateInfo) do keys[#keys + 1] = k end
    table.sort(keys)
    for _, mod_id in ipairs(keys) do
        local e = UpdateInfo[mod_id]
        f:write(string.format("    [%q] = { current=%q, latest=%q, checked_at=%d, status=%q },\n",
            mod_id, e.current or "", e.latest or "", e.checked_at or 0, e.status or "ok"))
    end
    f:write("}\n")
    f:close()

    os.remove(API.UPDATE_CACHE_PATH)
    local ok = os.rename(tmp, API.UPDATE_CACHE_PATH)
    if not ok then
        log(string.format("UpdateCheck: cache rename failed; cache may be stale"))
        return false
    end
    return true
end

local function ParseTagName(body)
    return body:match("\"tag_name\"%s*:%s*\"([^\"]+)\"")
end

local function FireRequest(mod_id, repo)
    local httpCDO = StaticFindObject("/Script/HttpBlueprint.Default__HttpRequestProxyObject")
    if not (httpCDO and httpCDO:IsValid()) then
        log("UpdateCheck: HttpRequestProxyObject CDO missing")
        return false
    end
    if not (UpdateTrampolinePC and UpdateTrampolinePC:IsValid()) then
        log("UpdateCheck: trampoline PC invalidated")
        return false
    end
    local url = "https://api.github.com/repos/" .. repo .. "/releases/latest"
    local ok, err = pcall(function()
        local proxy = httpCDO:CreateHttpRequestProxyObject(url, "GET", {}, "")
        if not (proxy and proxy:IsValid()) then
            error("CreateHttpRequestProxyObject returned nil/invalid")
        end
        proxy.OnRequestComplete:Add(UpdateTrampolinePC, "ServerExec")
    end)
    if not ok then
        log(string.format("UpdateCheck: FireRequest(%s) failed: %s", mod_id, tostring(err)))
        return false
    end
    UpdateInFlightStartedAt = NowSeconds()
    log(string.format("UpdateCheck: fetching %s for %s", url, mod_id))
    return true
end

local function AdvanceQueue()
    if UpdateInFlight then return end
    while #UpdateFetchQueue > 0 do
        local item = table.remove(UpdateFetchQueue, 1)
        UpdateInFlight = item.mod_id
        if FireRequest(item.mod_id, item.repo) then return end

        UpdateInfo[item.mod_id] = {
            current = (Registrations[item.mod_id] or {}).version or "",
            latest = (UpdateInfo[item.mod_id] or {}).latest or "",
            checked_at = NowSeconds(),
            status = "fire_error",
        }
        UpdateInFlight = nil
        UpdateInFlightStartedAt = nil
    end
    if not UpdateInFlight then SaveCache() end
end

local function HandleResponseBody(body)
    local mod_id = UpdateInFlight
    UpdateInFlight = nil
    UpdateInFlightStartedAt = nil
    if not mod_id then return end

    local manifest = Registrations[mod_id]
    if not manifest then
        log(string.format("UpdateCheck: response for unknown mod %s; discarding", mod_id))
        AdvanceQueue()
        return
    end

    local tag = ParseTagName(body)
    if not tag then

        local prev = UpdateInfo[mod_id] or {}
        UpdateInfo[mod_id] = {
            current    = manifest.version or "",
            latest     = prev.latest or "",
            checked_at = NowSeconds(),
            status     = "no_tag",
        }
        log(string.format("UpdateCheck: %s: no tag_name in response (preview: %s)",
            mod_id, body:sub(1, 200):gsub("[\r\n]", " ")))
        AdvanceQueue()
        return
    end

    local cmp = API.CompareVersions(tag, manifest.version or "0")
    UpdateInfo[mod_id] = {
        current    = manifest.version or "",
        latest     = tag,
        checked_at = NowSeconds(),
        status     = (cmp > 0) and "update_available" or "up_to_date",
    }
    if cmp > 0 then
        log(string.format("UpdateCheck: %s: UPDATE AVAILABLE  v%s -> v%s",
            mod_id, manifest.version or "?", tag))
    else
        log(string.format("UpdateCheck: %s: up to date (v%s, latest v%s)",
            mod_id, manifest.version or "?", tag))
    end
    AdvanceQueue()
end

local function InstallHook()
    if UpdateHookInstalled then return true end
    local ok, err = pcall(function()
        RegisterHook(UPDATE_TRAMPOLINE_PATH, function(ctx, strParam)

            if not UpdateInFlight then return end
            local readOk, body = pcall(function() return strParam:Get():ToString() end)
            if not readOk or type(body) ~= "string" or body == "" then return end
            local lead = body:sub(1, 1)
            if lead ~= "{" and lead ~= "[" then return end
            HandleResponseBody(body)
        end)
    end)
    if ok then
        UpdateHookInstalled = true
        log(string.format("UpdateCheck: trampoline hook installed at %s", UPDATE_TRAMPOLINE_PATH))
    else
        log(string.format("UpdateCheck: hook install FAILED: %s", tostring(err)))
    end
    return ok
end

local function InitUpdateCheck()
    if UpdateInitDone then return end

    if not CheckForUpdatesEnabled() then return end
    local pc = UEHelpers.GetPlayerController()
    if not (pc and pc:IsValid()) then return end

    HydrateCache()

    local pruned = {}
    for mod_id, _ in pairs(UpdateInfo) do
        if not Registrations[mod_id] then
            pruned[#pruned + 1] = mod_id
            UpdateInfo[mod_id] = nil
        end
    end
    if #pruned > 0 then
        log(string.format("UpdateCheck: pruned %d orphan cache entr%s (mod uninstalled): %s",
            #pruned, (#pruned == 1) and "y" or "ies", table.concat(pruned, ", ")))
    end

    UpdateTrampolinePC = pc
    if not InstallHook() then return end

    local now = NowSeconds()
    local queued, cached = 0, 0
    for mod_id, manifest in pairs(Registrations) do
        local repo = manifest.github
        if type(repo) == "string" and repo ~= "" then
            local prev = UpdateInfo[mod_id]
            local is_fresh = prev and prev.checked_at
                and (now - prev.checked_at < API.UPDATE_TTL_SECONDS)
                and (prev.status == "up_to_date" or prev.status == "update_available")
            if is_fresh then
                cached = cached + 1
                if prev.status == "update_available" then
                    log(string.format("UpdateCheck: %s (cached, age %ds): UPDATE AVAILABLE v%s -> v%s",
                        mod_id, now - prev.checked_at, prev.current or "?", prev.latest or "?"))
                else
                    log(string.format("UpdateCheck: %s (cached, age %ds): up to date (v%s)",
                        mod_id, now - prev.checked_at, prev.latest or "?"))
                end
            else
                UpdateFetchQueue[#UpdateFetchQueue + 1] = { mod_id = mod_id, repo = repo }
                queued = queued + 1
            end
        end
    end
    log(string.format("UpdateCheck: init complete - %d fresh from cache, %d queued for fetch",
        cached, queued))
    UpdateInitDone = true
    AdvanceQueue()
end

LoopInGameThreadWithDelay(1000, function()
    if not UpdateInitDone then InitUpdateCheck() end

    if UpdateInFlight and UpdateInFlightStartedAt then
        local elapsed = NowSeconds() - UpdateInFlightStartedAt
        if elapsed > UPDATE_FETCH_TIMEOUT_SECONDS then
            local stuck_mod_id = UpdateInFlight
            log(string.format("UpdateCheck: %s timeout after %ds (no response received) - advancing queue",
                stuck_mod_id, elapsed))
            UpdateInFlight = nil
            UpdateInFlightStartedAt = nil
            local manifest = Registrations[stuck_mod_id]
            if manifest then
                local prev = UpdateInfo[stuck_mod_id] or {}
                UpdateInfo[stuck_mod_id] = {
                    current    = manifest.version or "",
                    latest     = prev.latest or "",
                    checked_at = NowSeconds(),
                    status     = "timeout",
                }
            end
            AdvanceQueue()
        end
    end
end)

log("=== Library loaded (v1.1.1) ===")
