local BFH = _G.BlightfallHelper
if not BFH then return end

local ACCENT = {0.62, 0.11, 1}
local BG = {0.025, 0.027, 0.034, 0.985}
local PANEL = {0.052, 0.055, 0.066, 0.98}
local MUTED = {0.58, 0.61, 0.69}
local WHITE = {0.94, 0.95, 0.98}

local function Backdrop(frame, color)
    frame:SetBackdrop({
        bgFile="Interface\\Buttons\\WHITE8X8",
        edgeFile="Interface\\Buttons\\WHITE8X8",
        edgeSize=1
    })
    frame:SetBackdropColor(unpack(color or PANEL))
    frame:SetBackdropBorderColor(0.15, 0.16, 0.19, 1)
end

local function FS(parent, text, size, color)
    local f = parent:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    f:SetFont("Fonts\\FRIZQT__.TTF", size or 12, "")
    f:SetText(text or "")
    if color then f:SetTextColor(unpack(color)) end
    return f
end

local function AddTooltip(widget, title, body)
    widget:EnableMouse(true)
    widget:SetScript("OnEnter", function(self)
        GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
        GameTooltip:AddLine(title, 1, 1, 1)
        GameTooltip:AddLine(body, 0.82, 0.84, 0.90, true)
        GameTooltip:Show()
    end)
    widget:SetScript("OnLeave", function() GameTooltip:Hide() end)
end

local function HelpIcon(parent, x, y, title, body)
    local b = CreateFrame("Button", nil, parent, "BackdropTemplate")
    b:SetSize(18, 18)
    b:SetPoint("TOPLEFT", x, y)
    b:SetBackdrop({
        bgFile="Interface\\Buttons\\WHITE8X8",
        edgeFile="Interface\\Buttons\\WHITE8X8",
        edgeSize=1
    })
    b:SetBackdropColor(0.10,0.105,0.12,1)
    b:SetBackdropBorderColor(0.25,0.26,0.30,1)
    local t = FS(b, "?", 12, {0.82,0.84,0.90})
    t:SetPoint("CENTER")
    AddTooltip(b, title, body)
    return b
end

local function Button(parent, text, w, h, onClick)
    local b = CreateFrame("Button", nil, parent, "BackdropTemplate")
    b:SetSize(w or 120, h or 28)
    Backdrop(b, {0.075,0.078,0.092,1})
    local t = FS(b, text, 12, WHITE)
    t:SetPoint("CENTER")
    b.label = t
    b:SetScript("OnClick", onClick)
    b:SetScript("OnEnter", function(self) self:SetBackdropBorderColor(unpack(ACCENT)) end)
    b:SetScript("OnLeave", function(self) self:SetBackdropBorderColor(0.15,0.16,0.19,1) end)
    return b
end

local function Checkbox(parent, label, getter, setter)
    local b = CreateFrame("CheckButton", nil, parent, "BackdropTemplate")
    b:SetSize(20, 20)
    b:SetBackdrop({
        bgFile="Interface\\Buttons\\WHITE8X8",
        edgeFile="Interface\\Buttons\\WHITE8X8",
        edgeSize=1
    })
    b:SetBackdropColor(0.05,0.055,0.065,1)
    b:SetBackdropBorderColor(0.3,0.31,0.35,1)

    local mark = b:CreateTexture(nil, "ARTWORK")
    mark:SetPoint("CENTER")
    mark:SetSize(12,12)
    mark:SetColorTexture(unpack(ACCENT))
    b.mark = mark

    local txt = FS(parent, label, 12, {0.9,0.91,0.94})
    txt:SetPoint("LEFT", b, "RIGHT", 10, 0)

    function b:Refresh()
        mark:SetShown(getter())
    end

    b:SetScript("OnClick", function()
        setter(not getter())
        b:Refresh()
        BFH:RefreshConfig()
    end)

    return b
end

local function Slider(parent, label, minv, maxv, step, getter, setter, format)
    local wrap = CreateFrame("Frame", nil, parent)
    wrap:SetSize(300, 50)

    local l = FS(wrap, label, 11, {0.73,0.76,0.82})
    l:SetPoint("TOPLEFT", 0, 0)

    local val = FS(wrap, "", 11, {0.78,0.38,1})
    val:SetPoint("TOPRIGHT", 0, 0)

    local s = CreateFrame("Slider", nil, wrap, "BackdropTemplate")
    s:SetPoint("TOPLEFT", 0, -21)
    s:SetPoint("TOPRIGHT", 0, -21)
    s:SetHeight(12)
    s:SetOrientation("HORIZONTAL")
    s:SetMinMaxValues(minv,maxv)
    s:SetValueStep(step)
    s:SetObeyStepOnDrag(true)
    s:SetBackdrop({
        bgFile="Interface\\Buttons\\WHITE8X8",
        edgeFile="Interface\\Buttons\\WHITE8X8",
        edgeSize=1
    })
    s:SetBackdropColor(0.04,0.045,0.055,1)
    s:SetBackdropBorderColor(0.22,0.23,0.27,1)

    local thumb = s:CreateTexture(nil, "OVERLAY")
    thumb:SetColorTexture(unpack(ACCENT))
    thumb:SetSize(10,22)
    s:SetThumbTexture(thumb)

    local changing

    function wrap:Refresh()
        changing = true
        local v = getter()
        s:SetValue(v)
        val:SetText(format and format(v) or tostring(v))
        changing = false
    end

    s:SetScript("OnValueChanged", function(_, v)
        if changing then return end
        v = math.floor((v / step) + 0.5) * step
        setter(v)
        val:SetText(format and format(v) or tostring(v))
        BFH:ApplyDisplaySettings()
    end)

    return wrap
end

local function Dropdown(parent, label, options, getter, setter, onChange)
    local wrap = CreateFrame("Frame", nil, parent)
    wrap:SetSize(300, 54)

    local l = FS(wrap, label, 11, {0.73,0.76,0.82})
    l:SetPoint("TOPLEFT", 0, 0)

    local b = Button(wrap, "", 300, 28, nil)
    b:SetPoint("TOPLEFT", 0, -19)

    b:SetScript("OnClick", function(self)
        MenuUtil.CreateContextMenu(self, function(owner, rootDescription)
            for _, opt in ipairs(options) do
                rootDescription:CreateRadio(
                    opt.text,
                    function() return getter() == opt.value end,
                    function()
                        setter(opt.value)
                        wrap:Refresh()
                        BFH:ApplyDisplaySettings()
                        if onChange then onChange(opt.value) end
                        BFH:RefreshConfig()
                    end
                )
            end
        end)
    end)

    function wrap:Refresh()
        local v = getter()
        local text = tostring(v)
        for _, opt in ipairs(options) do
            if opt.value == v then text = opt.text break end
        end
        b.label:SetText(text)
    end

    return wrap
end

local function PageTitle(page, titleText, desc)
    local t = FS(page, titleText, 22, WHITE)
    t:SetPoint("TOPLEFT", 28, -28)

    local d = FS(page, desc, 11, MUTED)
    d:SetPoint("TOPLEFT", 30, -58)
end

local function MakeScrollablePage(parent)
    local scroll = CreateFrame("ScrollFrame", nil, parent, "UIPanelScrollFrameTemplate")
    scroll:SetAllPoints()

    local child = CreateFrame("Frame", nil, scroll)
    child:SetSize(700, 760)
    scroll:SetScrollChild(child)

    scroll:SetScript("OnSizeChanged", function(_, width)
        child:SetWidth(math.max(640, width - 32))
    end)

    return child
end

-- Font discovery: WoW defaults + LibSharedMedia + optional local copies.
BFH.localFontCandidates = {
    {"Arial Bold", "Interface\\AddOns\\BlightfallHelper\\Fonts\\Arial Bold.ttf"},
    {"Arial Narrow", "Interface\\AddOns\\BlightfallHelper\\Fonts\\Arial Narrow.ttf"},
    {"Avant Garde Naowh", "Interface\\AddOns\\BlightfallHelper\\Fonts\\Avant Garde Naowh.ttf"},
    {"Barlow Condensed", "Interface\\AddOns\\BlightfallHelper\\Fonts\\Barlow Condensed.ttf"},
    {"Changa", "Interface\\AddOns\\BlightfallHelper\\Fonts\\Changa.ttf"},
    {"Cinzel Decorative", "Interface\\AddOns\\BlightfallHelper\\Fonts\\Cinzel Decorative.ttf"},
    {"Exo", "Interface\\AddOns\\BlightfallHelper\\Fonts\\Exo.otf"},
    {"Expressway Bold", "Interface\\AddOns\\BlightfallHelper\\Fonts\\Expressway Bold.ttf"},
    {"Expressway", "Interface\\AddOns\\BlightfallHelper\\Fonts\\Expressway.ttf"},
    {"FiraSans Bold", "Interface\\AddOns\\BlightfallHelper\\Fonts\\FiraSans Bold.ttf"},
    {"FiraSans Light", "Interface\\AddOns\\BlightfallHelper\\Fonts\\FiraSans Light.ttf"},
    {"FiraSans Medium", "Interface\\AddOns\\BlightfallHelper\\Fonts\\FiraSans Medium.ttf"},
    {"Future X Black", "Interface\\AddOns\\BlightfallHelper\\Fonts\\Future X Black.otf"},
    {"Gotham Narrow Ultra", "Interface\\AddOns\\BlightfallHelper\\Fonts\\Gotham Narrow Ultra.ttf"},
    {"Gotham Narrow", "Interface\\AddOns\\BlightfallHelper\\Fonts\\Gotham Narrow.otf"},
    {"Homespun", "Interface\\AddOns\\BlightfallHelper\\Fonts\\Homespun.ttf"},
    {"KMT Kimberley", "Interface\\AddOns\\BlightfallHelper\\Fonts\\KMT Kimberley.otf"},
    {"KMT Ninja Naruto", "Interface\\AddOns\\BlightfallHelper\\Fonts\\KMT Ninja Naruto.ttf"},
    {"Poppins", "Interface\\AddOns\\BlightfallHelper\\Fonts\\Poppins.ttf"},
    {"Russo One", "Interface\\AddOns\\BlightfallHelper\\Fonts\\Russo One.ttf"},
    {"Ubuntu", "Interface\\AddOns\\BlightfallHelper\\Fonts\\Ubuntu.ttf"},
}

function BFH:GetAvailableFonts()
    local result, seen = {}, {}

    local function Add(name, path)
        if not name or not path or seen[path] then return end
        seen[path] = true
        table.insert(result, {name=name, path=path})
    end

    -- Always-available Blizzard fonts.
    Add("Friz Quadrata", "Fonts\\FRIZQT__.TTF")
    Add("Arial Narrow", "Fonts\\ARIALN.TTF")
    Add("Morpheus", "Fonts\\MORPHEUS.TTF")
    Add("Skurri", "Fonts\\SKURRI.TTF")

    -- Fonts exposed by WeakAuras/SharedMedia/other addons.
    local libStub = _G.LibStub
    if libStub then
        local ok, lsm = pcall(libStub, "LibSharedMedia-3.0", true)
        if ok and lsm and lsm.HashTable then
            local okFonts, fonts = pcall(lsm.HashTable, lsm, "font")
            if okFonts and type(fonts) == "table" then
                for name, path in pairs(fonts) do
                    Add(name, path)
                end
            end
        end
    end

    -- Optional standalone font files copied into BlightfallHelper\\Fonts.
    local probe = self.fontProbe
    if not probe then
        probe = UIParent:CreateFontString(nil, "OVERLAY")
        self.fontProbe = probe
    end

    for _, item in ipairs(self.localFontCandidates) do
        local ok, loaded = pcall(probe.SetFont, probe, item[2], 12, "")
        if ok and loaded then
            Add(item[1], item[2])
        end
    end

    table.sort(result, function(a,b)
        return a.name:lower() < b.name:lower()
    end)

    return result
end

function BFH:OpenFontPicker(anchor)
    if not self.fontPicker then
        local p = CreateFrame("Frame", "BlightfallHelperFontPicker", UIParent, "BackdropTemplate")
        p:SetSize(420, 500)
        p:SetFrameStrata("TOOLTIP")
        p:SetClampedToScreen(true)
        p:EnableMouse(true)
        Backdrop(p, {0.025,0.027,0.034,0.995})
        self.fontPicker = p

        local title = FS(p, "Choose Font", 16, WHITE)
        title:SetPoint("TOPLEFT", 16, -16)

        -- Proper close button so this popup can always be dismissed.
        local close = Button(p, "×", 30, 30, function()
            p:Hide()
        end)
        close:SetPoint("TOPRIGHT", -12, -12)

        local search = CreateFrame("EditBox", nil, p, "BackdropTemplate")
        search:SetSize(370, 30)
        search:SetPoint("TOPLEFT", 16, -52)
        Backdrop(search, {0.04,0.043,0.052,1})
        search:SetFontObject("GameFontHighlightSmall")
        search:SetTextInsets(10,8,0,0)
        search:SetAutoFocus(false)
        p.search = search

        local placeholder = FS(search, "Search fonts...", 11, MUTED)
        placeholder:SetPoint("LEFT", 10, 0)

        local scroll = CreateFrame("ScrollFrame", nil, p, "UIPanelScrollFrameTemplate")
        scroll:SetPoint("TOPLEFT", 16, -92)
        scroll:SetPoint("BOTTOMRIGHT", -34, 16)

        local child = CreateFrame("Frame", nil, scroll)
        child:SetWidth(350)
        child:SetHeight(1)
        scroll:SetScrollChild(child)
        p.child = child
        p.rows = {}

        local empty = FS(child, "No fonts found.", 12, MUTED)
        empty:SetPoint("TOPLEFT", 8, -8)
        empty:Hide()
        p.empty = empty

        local function RefreshRows()
            placeholder:SetShown(search:GetText() == "")

            local q = (search:GetText() or ""):lower()
            local fonts = BFH:GetAvailableFonts()
            local matches = {}

            for _, font in ipairs(fonts) do
                if q == "" or font.name:lower():find(q, 1, true) then
                    table.insert(matches, font)
                end
            end

            local y = -4
            for i, font in ipairs(matches) do
                local row = p.rows[i]
                if not row then
                    row = CreateFrame("Button", nil, child, "BackdropTemplate")
                    row:SetSize(340, 36)
                    row:SetBackdrop({bgFile="Interface\\Buttons\\WHITE8X8"})
                    row:SetBackdropColor(0.055,0.058,0.070,0.80)

                    row.text = row:CreateFontString(nil, "OVERLAY")
                    row.text:SetPoint("LEFT", 10, 0)
                    row.text:SetPoint("RIGHT", -10, 0)
                    row.text:SetJustifyH("LEFT")
                    row.text:SetTextColor(0.95,0.96,0.99)

                    row:SetScript("OnEnter", function(self)
                        self:SetBackdropColor(0.11,0.065,0.16,0.95)
                    end)
                    row:SetScript("OnLeave", function(self)
                        self:SetBackdropColor(0.055,0.058,0.070,0.80)
                    end)

                    p.rows[i] = row
                end

                row:ClearAllPoints()
                row:SetPoint("TOPLEFT", 0, y)
                row.fontData = font
                row.text:SetText(font.name)

                local ok = row.text:SetFont(font.path, 15, "")
                if not ok then
                    row.text:SetFont("Fonts\\FRIZQT__.TTF", 15, "")
                end

                row:SetScript("OnClick", function(self)
                    BFH.db.font = self.fontData.path
                    BFH.db.fontName = self.fontData.name
                    BFH:ApplyDisplaySettings()
                    BFH.fontPicker:Hide()
                    BFH:RefreshConfig()
                end)

                row:Show()
                y = y - 40
            end

            for i = #matches + 1, #p.rows do
                p.rows[i]:Hide()
            end

            p.empty:SetShown(#matches == 0)
            child:SetHeight(math.max(40, -y + 8))
        end

        search:SetScript("OnTextChanged", RefreshRows)
        p.RefreshRows = RefreshRows

        -- Escape closes the popup too.
        p:SetPropagateKeyboardInput(true)
        p:EnableKeyboard(true)
        p:SetScript("OnKeyDown", function(self, key)
            if key == "ESCAPE" then
                self:Hide()
            end
        end)
    end

    self.fontPicker:ClearAllPoints()
    self.fontPicker:SetPoint("CENTER", UIParent, "CENTER", 0, 0)
    self.fontPicker.search:SetText("")
    self.fontPicker:RefreshRows()
    self.fontPicker:Show()
end

function BFH:InitializeConfig()
    local f = CreateFrame("Frame", "BlightfallHelperConfig", UIParent, "BackdropTemplate")
    f:SetSize(self.db.configWidth, self.db.configHeight)
    f:SetPoint("CENTER", UIParent, "CENTER", 0, 0)
    f:SetFrameStrata("DIALOG")
    f:SetClampedToScreen(true)
    f:SetMovable(true)
    f:SetResizable(true)
    f:SetResizeBounds(900, 600, 1300, 900)
    f:EnableMouse(true)
    Backdrop(f, BG)
    f:Hide()
    self.config = f

    local header = CreateFrame("Frame", nil, f, "BackdropTemplate")
    header:SetPoint("TOPLEFT")
    header:SetPoint("TOPRIGHT")
    header:SetHeight(78)
    header:SetBackdrop({bgFile="Interface\\Buttons\\WHITE8X8"})
    header:SetBackdropColor(0.035,0.038,0.048,1)
    header:EnableMouse(true)
    header:RegisterForDrag("LeftButton")
    header:SetScript("OnDragStart", function() f:StartMoving() end)
    header:SetScript("OnDragStop", function() f:StopMovingOrSizing() end)

    local title = FS(header, "BlightfallHelper", 24, WHITE)
    title:SetPoint("LEFT", 26, 10)

    local ver = FS(header, "v"..self.VERSION, 11, MUTED)
    ver:SetPoint("LEFT", title, "RIGHT", 10, -2)

    local sub = FS(header, "Unholy DK cast timing assistant", 11, MUTED)
    sub:SetPoint("TOPLEFT", 28, -52)

    local close = Button(header, "×", 38, 38, function()
        if BFH.fontPicker then BFH.fontPicker:Hide() end
        f:Hide()
        BFH:StopStage()
    end)
    close:SetPoint("TOPRIGHT", -18, -18)

    local search = CreateFrame("EditBox", nil, header, "BackdropTemplate")
    search:SetSize(250, 30)
    search:SetPoint("RIGHT", close, "LEFT", -18, 0)
    Backdrop(search, {0.025,0.028,0.035,1})
    search:SetAutoFocus(false)
    search:SetFontObject("GameFontHighlightSmall")
    search:SetTextInsets(10,8,0,0)
    local placeholder = FS(search, "Search settings...", 11, MUTED)
    placeholder:SetPoint("LEFT", 10, 0)
    search:SetScript("OnTextChanged", function(self)
        placeholder:SetShown(self:GetText() == "")
        BFH.searchText = (self:GetText() or ""):lower()
        BFH:RefreshSearch()
    end)

    local nav = CreateFrame("Frame", nil, f, "BackdropTemplate")
    nav:SetPoint("TOPLEFT", 0, -78)
    nav:SetPoint("BOTTOMLEFT")
    nav:SetWidth(205)
    nav:SetBackdrop({bgFile="Interface\\Buttons\\WHITE8X8"})
    nav:SetBackdropColor(0.02,0.022,0.028,0.98)

    local content = CreateFrame("Frame", nil, f)
    content:SetPoint("TOPLEFT", nav, "TOPRIGHT", 0, 0)
    content:SetPoint("BOTTOMRIGHT", -8, 8)
    self.content = content

    local resize = CreateFrame("Button", nil, f)
    resize:SetSize(24,24)
    resize:SetPoint("BOTTOMRIGHT", -2, 2)
    resize:EnableMouse(true)
    local rt = FS(resize, "◢", 18, {0.5,0.52,0.58})
    rt:SetPoint("CENTER")
    AddTooltip(resize, "Resize window", "Drag this corner to change the BlightfallHelper settings window size.")
    resize:SetScript("OnMouseDown", function()
        f:StartSizing("BOTTOMRIGHT")
    end)
    resize:SetScript("OnMouseUp", function()
        f:StopMovingOrSizing()
        BFH.db.configWidth = math.floor(f:GetWidth() + 0.5)
        BFH.db.configHeight = math.floor(f:GetHeight() + 0.5)
    end)

    self.pages = {}
    self.navButtons = {}
    self.currentPage = "General"

    local pageNames = {"General","Display","Timing","Sound","Fonts & Style","Profiles"}

    for i, name in ipairs(pageNames) do
        local b = Button(nav, name, 177, 38, function() BFH:ShowPage(name) end)
        b:SetPoint("TOPLEFT", 14, -20 - (i-1)*46)
        self.navButtons[name] = b

        local holder = CreateFrame("Frame", nil, content)
        holder:SetAllPoints()
        holder:Hide()

        local page = MakeScrollablePage(holder)
        self.pages[name] = {holder=holder, page=page}
    end

    local searchHolder = CreateFrame("Frame", nil, content)
    searchHolder:SetAllPoints()
    searchHolder:Hide()
    self.searchHolder = searchHolder

    local searchPage = MakeScrollablePage(searchHolder)
    self.searchPage = searchPage
    PageTitle(searchPage, "Search Results", "Click a result to open the relevant settings tab.")
    self.searchRows = {}

    local function Page(name)
        return self.pages[name].page
    end

    -- General
    do
        local p = Page("General")
        PageTitle(p, "General", "Core behaviour, minimap access and display positioning.")

        local c1 = Checkbox(p, "Enable BlightfallHelper",
            function() return self.db.enabled end,
            function(v) self.db.enabled = v; if not v then self:StopStage() end end)
        c1:SetPoint("TOPLEFT", 30, -105)
        HelpIcon(p, 360, -104, "Enable addon", "Turns the helper on or off without disabling the addon in WoW's addon list.")

        local c2 = Checkbox(p, "Lock display position",
            function() return self.db.locked end,
            function(v) self.db.locked = v; self:ApplyDisplaySettings() end)
        c2:SetPoint("TOPLEFT", 30, -155)
        HelpIcon(p, 360, -154, "Lock position", "Unlock this to drag the active timer/preview anywhere on your screen.")

        local c3 = Checkbox(p, "Show minimap button",
            function() return self.db.showMinimapButton end,
            function(v) self.db.showMinimapButton = v; self:UpdateMinimapButton() end)
        c3:SetPoint("TOPLEFT", 30, -205)
        HelpIcon(p, 360, -204, "Minimap button", "Shows the Blightfall icon on the minimap. Left-click it to open /bfh and drag it around the minimap edge.")

        local test = Button(p, "Preview Soul Reaper", 155, 30, function() self:ShowPreview("SOUL") end)
        test:SetPoint("TOPLEFT", 30, -275)

        local test2 = Button(p, "Preview Blightfall", 155, 30, function() self:ShowPreview("BLIGHT") end)
        test2:SetPoint("LEFT", test, "RIGHT", 12, 0)

        local stop = Button(p, "Stop Preview", 125, 30, function() self:StopStage() end)
        stop:SetPoint("LEFT", test2, "RIGHT", 12, 0)

        local reset = Button(p, "Reset Position", 145, 30, function()
            self.db.point = "CENTER"
            self.db.relativePoint = "CENTER"
            self.db.x = 0
            self.db.y = 0
            self:ApplyPosition()
        end)
        reset:SetPoint("TOPLEFT", 30, -325)

        self.pages.General.controls = {c1,c2,c3}
    end

    -- Display
    do
        local p = Page("Display")
        PageTitle(p, "Display", "Choose bar or icon mode and tune the on-screen element.")

        local mode = Dropdown(
            p, "Display mode",
            {{text="Progress Bar",value="BAR"},{text="Icon Only",value="ICON"}},
            function() return self.db.displayMode end,
            function(v) self.db.displayMode = v end
        )
        mode:SetPoint("TOPLEFT", 30, -105)
        HelpIcon(p, 350, -104, "Display mode", "Progress Bar shows the spell name and remaining time. Icon Only shows just the spell icon with the number underneath.")

        local s1 = Slider(p, "Overall scale", 0.5, 2.0, 0.05,
            function() return self.db.scale end,
            function(v) self.db.scale = v end,
            function(v) return string.format("%.2f",v) end)
        s1:SetPoint("TOPLEFT", 30, -190)

        local s2 = Slider(p, "Bar width", 100, 500, 5,
            function() return self.db.barWidth end,
            function(v) self.db.barWidth = v end,
            function(v) return string.format("%d px",v) end)
        s2:SetPoint("TOPLEFT", 370, -190)

        local s3 = Slider(p, "Bar height", 10, 60, 1,
            function() return self.db.barHeight end,
            function(v) self.db.barHeight = v end,
            function(v) return string.format("%d px",v) end)
        s3:SetPoint("TOPLEFT", 30, -275)

        local c1 = Checkbox(p, "Show spell icon on bar",
            function() return self.db.showIcon end,
            function(v) self.db.showIcon = v; self:ApplyDisplaySettings() end)
        c1:SetPoint("TOPLEFT", 370, -278)

        local pos = Dropdown(
            p, "Icon position",
            {{text="Left",value="LEFT"},{text="Right",value="RIGHT"}},
            function() return self.db.iconPosition end,
            function(v) self.db.iconPosition = v end
        )
        pos:SetPoint("TOPLEFT", 30, -355)

        local is = Slider(p, "Bar icon size", 10, 80, 1,
            function() return self.db.iconSize end,
            function(v) self.db.iconSize = v end,
            function(v) return string.format("%d px",v) end)
        is:SetPoint("TOPLEFT", 370, -355)

        local io = Slider(p, "Icon-only size", 32, 128, 2,
            function() return self.db.iconOnlySize end,
            function(v) self.db.iconOnlySize = v end,
            function(v) return string.format("%d px",v) end)
        io:SetPoint("TOPLEFT", 30, -440)

        local spark = Checkbox(p, "Show moving marker",
            function() return self.db.showSpark end,
            function(v) self.db.showSpark = v; self:ApplyDisplaySettings() end)
        spark:SetPoint("TOPLEFT", 370, -443)
        HelpIcon(p, 590, -442, "Moving marker", "A small 2-pixel marker that moves with the timer. This replaces the larger glow from the previous version.")

        local bg = Slider(p, "Background opacity", 0, 1, 0.05,
            function() return self.db.backgroundAlpha end,
            function(v) self.db.backgroundAlpha = v end,
            function(v) return string.format("%d%%",v*100) end)
        bg:SetPoint("TOPLEFT", 30, -525)

        self.pages.Display.controls = {mode,s1,s2,s3,c1,pos,is,io,spark,bg}
    end

    -- Timing
    do
        local p = Page("Timing")
        PageTitle(p, "Timing", "Adjust the recommended time between each cast.")

        local s1 = Slider(p, "Soul Reaper delay after Dark Transformation", 1, 12, 0.1,
            function() return self.db.soulDelay end,
            function(v) self.db.soulDelay = v end,
            function(v) return string.format("%.1fs",v) end)
        s1:SetPoint("TOPLEFT", 30, -120)
        HelpIcon(p, 350, -119, "Soul Reaper delay", "How long the Soul Reaper timer runs after Dark Transformation. Casting Soul Reaper early immediately moves on to Blightfall.")

        local s2 = Slider(p, "Blightfall delay after Soul Reaper", 1, 12, 0.1,
            function() return self.db.blightDelay end,
            function(v) self.db.blightDelay = v end,
            function(v) return string.format("%.1fs",v) end)
        s2:SetPoint("TOPLEFT", 30, -215)
        HelpIcon(p, 350, -214, "Blightfall delay", "How long the Blightfall timer runs after the first Soul Reaper following Dark Transformation.")

        local s3 = Slider(p, "Spoken countdown starts at", 1, 10, 1,
            function() return self.db.countdownStart end,
            function(v) self.db.countdownStart = v end,
            function(v) return tostring(v) end)
        s3:SetPoint("TOPLEFT", 30, -310)
        HelpIcon(p, 350, -309, "Countdown start", "Choose how many of the final seconds are spoken. The included custom number pack supports 1 through 10.")

        local precision = Dropdown(
            p, "Timer precision",
            {{text="Whole seconds",value=0},{text="1 decimal",value=1},{text="2 decimals",value=2}},
            function() return self.db.precision end,
            function(v) self.db.precision = v end
        )
        precision:SetPoint("TOPLEFT", 30, -410)

        self.pages.Timing.controls = {s1,s2,s3,precision}
    end

    -- Sound
    do
        local p = Page("Sound")
        PageTitle(p, "Sound", "Choose your uploaded number files or WoW's built-in text-to-speech.")

        local enabled = Checkbox(p, "Enable spoken countdown",
            function() return self.db.soundEnabled end,
            function(v) self.db.soundEnabled = v; if not v then self:StopTTS() end end)
        enabled:SetPoint("TOPLEFT", 30, -105)

        local mode = Dropdown(
            p, "Countdown audio mode",
            {{text="Custom 1-10 voice files",value="FILES"},{text="WoW Text-to-Speech",value="TTS"}},
            function() return self.db.audioMode end,
            function(v) self.db.audioMode = v; if v ~= "TTS" then self:StopTTS() end end
        )
        mode:SetPoint("TOPLEFT", 30, -170)
        HelpIcon(p, 350, -169, "Audio mode", "Custom files use the 1-10 .ogg files bundled with BlightfallHelper. TTS uses WoW's text-to-speech engine and has its own volume control.")

        local channel = Dropdown(
            p, "Custom-file WoW sound channel",
            {
                {text="Master",value="Master"},
                {text="SFX",value="SFX"},
                {text="Dialog",value="Dialog"},
                {text="Music",value="Music"},
                {text="Ambience",value="Ambience"},
            },
            function() return self.db.soundChannel end,
            function(v) self.db.soundChannel = v end
        )
        channel:SetPoint("TOPLEFT", 30, -250)
        HelpIcon(p, 350, -249, "Sound channel", "Only applies to Custom Voice Files. These files follow the selected WoW mixer channel's volume.")

        local volume = Slider(p, "TTS volume", 0, 100, 1,
            function() return self.db.ttsVolume end,
            function(v) self.db.ttsVolume = v end,
            function(v) return string.format("%d%%",v) end)
        volume:SetPoint("TOPLEFT", 370, -250)
        HelpIcon(p, 690, -249, "TTS volume", "Independent volume passed to WoW's text-to-speech system. It does not alter your normal SFX/Music/Ambience sliders.")

        local rate = Slider(p, "TTS voice speed", -10, 10, 1,
            function() return self.db.ttsRate end,
            function(v) self.db.ttsRate = v end,
            function(v) return tostring(v) end)
        rate:SetPoint("TOPLEFT", 370, -335)

        local voiceButton = Button(p, "Choose TTS Voice", 180, 30, function(selfButton)
            local voices = BFH:GetTTSVoices()
            if #voices == 0 then
                print("|cff9f1cffBlightfallHelper:|r No WoW TTS voices are currently available.")
                return
            end
            MenuUtil.CreateContextMenu(selfButton, function(owner, rootDescription)
                for _, voice in ipairs(voices) do
                    local label = voice.name or ("Voice "..tostring(voice.voiceID))
                    rootDescription:CreateRadio(
                        label,
                        function() return BFH.db.ttsVoiceID == voice.voiceID end,
                        function()
                            BFH.db.ttsVoiceID = voice.voiceID
                            BFH:RefreshConfig()
                        end
                    )
                end
            end)
        end)
        voiceButton:SetPoint("TOPLEFT", 30, -340)

        local testTTS = Button(p, "Test TTS", 100, 30, function()
            local old = self.db.audioMode
            self.db.audioMode = "TTS"
            self:PlayCountdown(4)
            self.db.audioMode = old
        end)
        testTTS:SetPoint("LEFT", voiceButton, "RIGHT", 12, 0)

        local note = FS(p, "Test bundled custom sounds:", 11, MUTED)
        note:SetPoint("TOPLEFT", 30, -430)

        local x, y = 30, -460
        for n = 10, 1, -1 do
            local b = Button(p, tostring(n), 48, 30, function()
                local old = self.db.audioMode
                self.db.audioMode = "FILES"
                self:PlayCountdown(n)
                self.db.audioMode = old
            end)
            b:SetPoint("TOPLEFT", x, y)
            x = x + 56
            if n == 6 then x, y = 30, y - 42 end
        end

        self.pages.Sound.controls = {enabled,mode,channel,volume,rate}
    end

    -- Fonts & Style
    do
        local p = Page("Fonts & Style")
        PageTitle(p, "Fonts & Style", "Typography and visual finishing touches.")

        local fontLabel = FS(p, "Font", 11, {0.73,0.76,0.82})
        fontLabel:SetPoint("TOPLEFT", 30, -110)

        local fontButton = Button(p, self.db.fontName or "Friz Quadrata", 320, 32, function(selfButton)
            BFH:OpenFontPicker(selfButton)
        end)
        fontButton:SetPoint("TOPLEFT", 30, -132)
        self.fontButton = fontButton
        HelpIcon(p, 365, -137, "Font library", "Shows WoW fonts, LibSharedMedia fonts from addons such as WeakAuras, plus compatible files placed in BlightfallHelper\\Fonts.")

        local s1 = Slider(p, "Spell-name font size", 8, 32, 1,
            function() return self.db.labelFontSize end,
            function(v) self.db.labelFontSize = v end,
            function(v) return string.format("%d px",v) end)
        s1:SetPoint("TOPLEFT", 30, -220)

        local s2 = Slider(p, "Timer font size", 8, 32, 1,
            function() return self.db.timerFontSize end,
            function(v) self.db.timerFontSize = v end,
            function(v) return string.format("%d px",v) end)
        s2:SetPoint("TOPLEFT", 370, -220)

        local border = Checkbox(p, "Show border",
            function() return self.db.borderEnabled end,
            function(v) self.db.borderEnabled = v; self:ApplyDisplaySettings() end)
        border:SetPoint("TOPLEFT", 30, -310)

        local colours = FS(
            p,
            "Soul Reaper: #1C28FF   •   Blightfall: #9F1CFF   •   Under 4 seconds: #FF0010",
            11,
            MUTED
        )
        colours:SetPoint("TOPLEFT", 30, -380)

        self.pages["Fonts & Style"].controls = {s1,s2,border}
    end

    -- Profiles
    do
        local p = Page("Profiles")
        PageTitle(p, "Profiles", "Save and switch between complete BlightfallHelper setups.")

        local current = FS(p, "Current profile: Default", 14, WHITE)
        current:SetPoint("TOPLEFT", 30, -115)
        self.currentProfileText = current

        local name = CreateFrame("EditBox", nil, p, "BackdropTemplate")
        name:SetSize(280, 30)
        name:SetPoint("TOPLEFT", 30, -165)
        Backdrop(name, {0.03,0.033,0.04,1})
        name:SetAutoFocus(false)
        name:SetFontObject("GameFontHighlightSmall")
        name:SetTextInsets(10,8,0,0)
        self.profileNameBox = name

        local save = Button(p, "Save / Update", 130, 30, function()
            local n = (name:GetText() or ""):match("^%s*(.-)%s*$")
            if n == "" then n = self.db.activeProfile or "Default" end
            self.db.profiles[n] = self:CaptureProfile()
            self.db.activeProfile = n
            self:RefreshConfig()
        end)
        save:SetPoint("LEFT", name, "RIGHT", 12, 0)

        local load = Button(p, "Load", 90, 30, function()
            local n = (name:GetText() or ""):match("^%s*(.-)%s*$")
            if self.db.profiles[n] then
                self:ApplyProfile(self.db.profiles[n])
                self.db.activeProfile = n
                self:RefreshConfig()
            end
        end)
        load:SetPoint("LEFT", save, "RIGHT", 12, 0)

        local reset = Button(p, "Reset to Defaults", 155, 30, function()
            self:ResetToDefaults()
            self:RefreshConfig()
        end)
        reset:SetPoint("TOPLEFT", 30, -225)
    end

    self.searchIndex = {
        {name="Enable BlightfallHelper", page="General", keywords="enable addon on off"},
        {name="Lock display position", page="General", keywords="lock unlock drag position"},
        {name="Show minimap button", page="General", keywords="minimap icon button"},
        {name="Display mode", page="Display", keywords="bar icon only display"},
        {name="Overall scale", page="Display", keywords="scale size"},
        {name="Bar width", page="Display", keywords="width bar"},
        {name="Bar height", page="Display", keywords="height bar"},
        {name="Show spell icon", page="Display", keywords="icon bar spell"},
        {name="Icon position", page="Display", keywords="icon left right"},
        {name="Icon size", page="Display", keywords="icon size"},
        {name="Moving marker", page="Display", keywords="spark marker glow"},
        {name="Background opacity", page="Display", keywords="background opacity alpha"},
        {name="Soul Reaper delay", page="Timing", keywords="soul reaper dark transformation timer delay"},
        {name="Blightfall delay", page="Timing", keywords="blightfall soul reaper timer delay"},
        {name="Countdown start", page="Timing", keywords="countdown start seconds voice"},
        {name="Timer precision", page="Timing", keywords="precision decimals timer"},
        {name="Audio mode", page="Sound", keywords="tts custom voice files sound"},
        {name="Sound channel", page="Sound", keywords="master sfx dialog music ambience channel"},
        {name="TTS volume", page="Sound", keywords="tts volume voice"},
        {name="TTS speed", page="Sound", keywords="tts speed rate voice"},
        {name="TTS voice", page="Sound", keywords="tts voice selection"},
        {name="Font", page="Fonts & Style", keywords="font typeface sharedmedia weakauras"},
        {name="Font sizes", page="Fonts & Style", keywords="font text size timer"},
        {name="Border", page="Fonts & Style", keywords="border style"},
        {name="Profiles", page="Profiles", keywords="profile preset save load"},
    }

    self:ShowPage("General")
end

function BFH:CaptureProfile()
    local omit = {
        profiles=true,
        activeProfile=true,
        configWidth=true,
        configHeight=true,
        minimapAngle=true,
    }
    local out = {}
    for k,v in pairs(self.db) do
        if not omit[k] then out[k] = self.DeepCopy(v) end
    end
    return out
end

function BFH:ApplyProfile(profile)
    for k,v in pairs(profile) do self.db[k] = self.DeepCopy(v) end
    self:ApplyDisplaySettings()
    self:UpdateMinimapButton()
end

function BFH:ResetToDefaults()
    local profiles = self.db.profiles
    local active = self.db.activeProfile
    local cw, ch = self.db.configWidth, self.db.configHeight
    local angle = self.db.minimapAngle

    for k in pairs(self.db) do self.db[k] = nil end
    self.CopyDefaults(self.defaults, self.db)

    self.db.profiles = profiles or {Default={}}
    self.db.activeProfile = active or "Default"
    self.db.configWidth, self.db.configHeight = cw or 1040, ch or 700
    self.db.minimapAngle = angle or 225

    self:ApplyDisplaySettings()
    self:UpdateMinimapButton()
end

function BFH:ShowPage(name)
    self.currentPage = name
    self.searchHolder:Hide()

    for n, data in pairs(self.pages) do
        data.holder:SetShown(n == name)
    end

    for n, b in pairs(self.navButtons) do
        if n == name then
            b:SetBackdropColor(0.12,0.055,0.18,1)
            b:SetBackdropBorderColor(unpack(ACCENT))
        else
            b:SetBackdropColor(0.075,0.078,0.092,1)
            b:SetBackdropBorderColor(0.15,0.16,0.19,1)
        end
    end

    self:RefreshConfig()
end

function BFH:RefreshSearch()
    if not self.searchHolder then return end
    local q = self.searchText or ""

    if q == "" then
        self.searchHolder:Hide()
        if self.pages[self.currentPage] then
            self.pages[self.currentPage].holder:Show()
        end
        return
    end

    for _, data in pairs(self.pages) do data.holder:Hide() end
    self.searchHolder:Show()

    local matches = {}
    for _, item in ipairs(self.searchIndex or {}) do
        local haystack = (item.name.." "..item.page.." "..item.keywords):lower()
        if haystack:find(q, 1, true) then table.insert(matches, item) end
    end

    local y = -105
    for i, item in ipairs(matches) do
        local row = self.searchRows[i]
        if not row then
            row = Button(self.searchPage, "", 600, 42, nil)
            row.label:ClearAllPoints()
            row.label:SetPoint("LEFT", 12, 0)
            row.label:SetJustifyH("LEFT")
            self.searchRows[i] = row
        end
        row:SetPoint("TOPLEFT", 30, y)
        row.label:SetText(item.name.."   |cff888a95"..item.page.."|r")
        row:SetScript("OnClick", function()
            self.searchText = ""
            self:ShowPage(item.page)
        end)
        row:Show()
        y = y - 48
    end

    for i = #matches + 1, #self.searchRows do self.searchRows[i]:Hide() end
    self.searchPage:SetHeight(math.max(760, -y + 100))
end

function BFH:RefreshConfig()
    if not self.config then return end

    for _, data in pairs(self.pages) do
        if data.controls then
            for _, control in ipairs(data.controls) do
                if control.Refresh then control:Refresh() end
            end
        end
    end

    if self.fontButton then
        self.fontButton.label:SetText(self.db.fontName or "Friz Quadrata")
        local path = self.db.font or "Fonts\\FRIZQT__.TTF"
        if not self.fontButton.label:SetFont(path, 12, "") then
            self.fontButton.label:SetFont("Fonts\\FRIZQT__.TTF", 12, "")
        end
    end

    if self.currentProfileText then
        self.currentProfileText:SetText("Current profile: "..(self.db.activeProfile or "Default"))
    end
end

function BFH:ToggleConfig()
    if not self.config then return end

    if self.config:IsShown() then
        if self.fontPicker then self.fontPicker:Hide() end
        self.config:Hide()
        self:StopStage()
    else
        -- Always open centered as requested.
        self.config:ClearAllPoints()
        self.config:SetPoint("CENTER", UIParent, "CENTER", 0, 0)
        self.config:SetSize(
            math.max(900, tonumber(self.db.configWidth) or 1040),
            math.max(600, tonumber(self.db.configHeight) or 700)
        )
        self.config:Show()
        self:RefreshConfig()
    end
end
