EbonholdHub.UI = EbonholdHub.UI or {}

local UI = EbonholdHub.UI

UI.Colors = {
    bg          = {0.07, 0.07, 0.09, 0.96},
    bgHeader    = {0.10, 0.10, 0.13, 1.00},
    border      = {0.18, 0.18, 0.22, 1.00},
    accent      = {0.30, 0.75, 0.95, 1.00},
    accentDim   = {0.30, 0.75, 0.95, 0.35},
    text        = {0.92, 0.92, 0.94, 1.00},
    textDim     = {0.55, 0.55, 0.60, 1.00},
    elementBg   = {0.12, 0.12, 0.15, 1.00},
    elementHov  = {0.16, 0.16, 0.20, 1.00},
    danger      = {0.85, 0.35, 0.35, 1.00},
}

local C = UI.Colors
local FONT = "Fonts\\FRIZQT__.TTF"
local FLAT = "Interface\\Buttons\\WHITE8X8"

UI.AddonIcon   = "Interface\\AddOns\\EbonholdHub\\media\\addon_icon"
UI.MinimapIcon = "Interface\\AddOns\\EbonholdHub\\media\\minimap_icon"

UI.FONT_SIZE_TITLE  = 13
UI.FONT_SIZE_NORMAL = 11
UI.FONT_SIZE_SMALL  = 10

function UI.SetFont(fontString, size, colorTable, outline)
    fontString:SetFont(FONT, size, outline and "OUTLINE" or "")
    if colorTable then
        fontString:SetTextColor(unpack(colorTable))
    end
end

function UI.HideAllFontStrings(frame)
    if not frame then return end
    local regions = { frame:GetRegions() }
    for i = 1, #regions do
        local region = regions[i]
        if region:IsObjectType("FontString") then
            region:Hide()
        end
    end
end

function UI.Fill(parent, colorTable, layer)
    local tex = parent:CreateTexture(nil, layer or "BACKGROUND")
    tex:SetTexture(FLAT)
    tex:SetVertexColor(unpack(colorTable))
    tex:SetAllPoints(parent)
    return tex
end

function UI.AddThinBorder(frame, colorTable, thickness)
    thickness = thickness or 1
    local c = colorTable

    local top = frame:CreateTexture(nil, "BORDER")
    top:SetTexture(FLAT); top:SetVertexColor(unpack(c))
    top:SetPoint("TOPLEFT", 0, 0); top:SetPoint("TOPRIGHT", 0, 0); top:SetHeight(thickness)

    local bottom = frame:CreateTexture(nil, "BORDER")
    bottom:SetTexture(FLAT); bottom:SetVertexColor(unpack(c))
    bottom:SetPoint("BOTTOMLEFT", 0, 0); bottom:SetPoint("BOTTOMRIGHT", 0, 0); bottom:SetHeight(thickness)

    local left = frame:CreateTexture(nil, "BORDER")
    left:SetTexture(FLAT); left:SetVertexColor(unpack(c))
    left:SetPoint("TOPLEFT", 0, 0); left:SetPoint("BOTTOMLEFT", 0, 0); left:SetWidth(thickness)

    local right = frame:CreateTexture(nil, "BORDER")
    right:SetTexture(FLAT); right:SetVertexColor(unpack(c))
    right:SetPoint("TOPRIGHT", 0, 0); right:SetPoint("BOTTOMRIGHT", 0, 0); right:SetWidth(thickness)

    return {top = top, bottom = bottom, left = left, right = right}
end

function UI.CreateLabel(parent, text, size, color)
    local label = parent:CreateFontString(nil, "OVERLAY")
    UI.SetFont(label, size or UI.FONT_SIZE_NORMAL, color or C.text)
    if text then label:SetText(text) end
    return label
end

function UI.CreatePanel(parent)
    local panel = CreateFrame("Frame", nil, parent)
    UI.Fill(panel, C.elementBg)
    UI.AddThinBorder(panel, C.border, 1)
    return panel
end

function UI.CreateWindow(name, width, height, title, opts)
    opts = opts or {}
    local f = CreateFrame("Frame", name, UIParent)
    f:SetSize(width, height)
    f:SetPoint("CENTER")
    f:SetMovable(true)
    f:EnableMouse(true)
    f:SetClampedToScreen(true)
    f:SetFrameStrata(opts.strata or "DIALOG")
    if opts.toplevel then f:SetToplevel(true) end

    UI.Fill(f, C.bg)
    UI.AddThinBorder(f, C.border, 1)

    local header = CreateFrame("Frame", nil, f)
    header:SetPoint("TOPLEFT", 0, 0)
    header:SetPoint("TOPRIGHT", 0, 0)
    header:SetHeight(28)
    UI.Fill(header, C.bgHeader)
    header:EnableMouse(true)
    header:SetScript("OnMouseDown", function() f:StartMoving() end)
    header:SetScript("OnMouseUp", function() f:StopMovingOrSizing() end)

    local sep = header:CreateTexture(nil, "ARTWORK")
    sep:SetTexture(FLAT)
    sep:SetVertexColor(unpack(C.border))
    sep:SetPoint("BOTTOMLEFT", 0, 0)
    sep:SetPoint("BOTTOMRIGHT", 0, 0)
    sep:SetHeight(1)

    local titleText = header:CreateFontString(nil, "OVERLAY")
    UI.SetFont(titleText, UI.FONT_SIZE_TITLE, C.text)
    titleText:SetPoint("LEFT", 12, 0)
    titleText:SetText(title or "")

    local close = CreateFrame("Button", nil, header)
    close:SetSize(20, 20)
    close:SetPoint("RIGHT", -6, 0)
    local closeText = close:CreateFontString(nil, "OVERLAY")
    UI.SetFont(closeText, 14, C.textDim)
    closeText:SetPoint("CENTER")
    closeText:SetText("x")
    close:SetScript("OnEnter", function() UI.SetFont(closeText, 14, C.danger) end)
    close:SetScript("OnLeave", function() UI.SetFont(closeText, 14, C.textDim) end)
    close:SetScript("OnClick", function() f:Hide() end)

    local content = CreateFrame("Frame", nil, f)
    content:SetPoint("TOPLEFT", header, "BOTTOMLEFT", 12, -10)
    content:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -12, 10)
    f.content = content
    f.header = header
    f.title = titleText
    f.closeButton = close

    if name then
        tinsert(UISpecialFrames, name)
    end

    return f
end

function UI.CreateButton(parent, width, height, text, opts)
    opts = opts or {}
    local btn = CreateFrame("Button", nil, parent)
    btn:SetSize(width, height)

    local bg = UI.Fill(btn, opts.bg or C.elementBg)
    local border = UI.AddThinBorder(btn, C.border, 1)

    local label = btn:CreateFontString(nil, "OVERLAY")
    UI.SetFont(label, UI.FONT_SIZE_NORMAL, opts.textColor or C.text)
    label:SetPoint("CENTER")
    label:SetText(text or "")

    local hoverColor = opts.hoverBorder or C.accent
    btn:SetScript("OnEnter", function()
        bg:SetVertexColor(unpack(C.elementHov))
        border.top:SetVertexColor(unpack(hoverColor))
        border.bottom:SetVertexColor(unpack(hoverColor))
        border.left:SetVertexColor(unpack(hoverColor))
        border.right:SetVertexColor(unpack(hoverColor))
    end)
    btn:SetScript("OnLeave", function()
        bg:SetVertexColor(unpack(opts.bg or C.elementBg))
        border.top:SetVertexColor(unpack(C.border))
        border.bottom:SetVertexColor(unpack(C.border))
        border.left:SetVertexColor(unpack(C.border))
        border.right:SetVertexColor(unpack(C.border))
    end)
    btn:SetScript("OnMouseDown", function() label:SetPoint("CENTER", 0, -1) end)
    btn:SetScript("OnMouseUp", function() label:SetPoint("CENTER", 0, 0) end)

    btn.label = label
    btn._bg = bg
    btn._border = border
    return btn
end

local function SetVisible(obj, shown)
    if shown then obj:Show() else obj:Hide() end
end

function UI.CreateCheckbox(parent, text, checked)
    local row = CreateFrame("Frame", nil, parent)
    row:SetHeight(16)

    local cb = CreateFrame("Button", nil, row)
    cb:SetSize(16, 16)
    cb:SetPoint("LEFT", 0, 0)
    UI.Fill(cb, C.elementBg)
    UI.AddThinBorder(cb, C.border, 1)

    local mark = cb:CreateTexture(nil, "OVERLAY")
    mark:SetTexture(FLAT)
    mark:SetVertexColor(unpack(C.accent))
    mark:SetPoint("TOPLEFT", 3, -3)
    mark:SetPoint("BOTTOMRIGHT", -3, 3)
    SetVisible(mark, checked)

    local labelHit = CreateFrame("Button", nil, row)
    labelHit:SetPoint("LEFT", cb, "RIGHT", 0, 0)
    labelHit:SetPoint("RIGHT", row, "RIGHT", 0, 0)
    labelHit:SetHeight(16)

    local label = labelHit:CreateFontString(nil, "OVERLAY")
    UI.SetFont(label, UI.FONT_SIZE_NORMAL, C.text)
    label:SetPoint("LEFT", labelHit, "LEFT", 6, 0)
    label:SetText(text or "")

    cb.checked = checked or false
    cb.mark = mark
    row.checkbox = cb
    row.label = label

    cb:SetScript("OnClick", function(self)
        self.checked = not self.checked
        SetVisible(mark, self.checked)
        if row.OnValueChanged then row.OnValueChanged(self.checked) end
    end)

    labelHit:SetScript("OnClick", function() cb:Click() end)

    function row:SetChecked(value)
        cb.checked = value and true or false
        SetVisible(mark, cb.checked)
    end

    function row:GetChecked()
        return cb.checked
    end

    return row
end

function UI.CreateSlider(parent, width, minVal, maxVal, defaultVal, opts)
    opts = opts or {}
    local step = opts.step or 1
    local format = opts.format or function(v) return tostring(math.floor(v + 0.5)) end

    local slider = CreateFrame("Slider", nil, parent)
    slider:SetSize(width, 14)
    slider:SetOrientation("HORIZONTAL")
    slider:SetMinMaxValues(minVal, maxVal)
    slider:SetValue(defaultVal or minVal)
    slider:SetValueStep(step)

    local track = slider:CreateTexture(nil, "BACKGROUND")
    track:SetTexture(FLAT)
    track:SetVertexColor(unpack(C.border))
    track:SetPoint("LEFT", 0, 0)
    track:SetPoint("RIGHT", 0, 0)
    track:SetHeight(2)

    local thumb = slider:CreateTexture(nil, "OVERLAY")
    thumb:SetTexture(FLAT)
    thumb:SetVertexColor(unpack(C.accent))
    thumb:SetSize(10, 14)
    slider:SetThumbTexture(thumb)

    local valueText = slider:CreateFontString(nil, "OVERLAY")
    UI.SetFont(valueText, UI.FONT_SIZE_SMALL, C.textDim)
    valueText:SetPoint("TOP", slider, "BOTTOM", 0, -2)

    local function Refresh(value)
        valueText:SetText(format(value))
    end

    slider:SetScript("OnValueChanged", function(self, value)
        Refresh(value)
        if self.OnValueChanged then self:OnValueChanged(value) end
    end)
    Refresh(defaultVal or minVal)

    slider.valueText = valueText
    return slider
end

function UI.CreateEditBox(parent, width, height, multiline)
    local holder = CreateFrame("Frame", nil, parent)
    holder:SetSize(width, height)
    UI.Fill(holder, C.elementBg)
    local border = UI.AddThinBorder(holder, C.border, 1)

    local edit = CreateFrame("EditBox", nil, holder)
    edit:SetPoint("TOPLEFT", 6, -4)
    edit:SetPoint("BOTTOMRIGHT", -6, 4)
    edit:SetAutoFocus(false)
    UI.SetFont(edit, UI.FONT_SIZE_NORMAL, C.text)
    edit:SetTextInsets(0, 0, 0, 0)
    if multiline then
        edit:SetMultiLine(true)
        edit:SetMaxLetters(0)
    end

    edit:SetScript("OnEditFocusGained", function()
        border.top:SetVertexColor(unpack(C.accent))
        border.bottom:SetVertexColor(unpack(C.accent))
        border.left:SetVertexColor(unpack(C.accent))
        border.right:SetVertexColor(unpack(C.accent))
    end)
    edit:SetScript("OnEditFocusLost", function()
        border.top:SetVertexColor(unpack(C.border))
        border.bottom:SetVertexColor(unpack(C.border))
        border.left:SetVertexColor(unpack(C.border))
        border.right:SetVertexColor(unpack(C.border))
    end)
    edit:SetScript("OnEscapePressed", function(self) self:ClearFocus() end)
    if not multiline then
        edit:SetScript("OnEnterPressed", function(self) self:ClearFocus() end)
    end

    holder.editBox = edit
    holder.border = border
    return holder
end

function UI.CreateScrollEditBox(parent)
    local holder = UI.CreateScrollFrame(parent)
    holder:SetPoint("TOPLEFT", parent, "TOPLEFT", 0, 0)
    holder:SetPoint("BOTTOMRIGHT", parent, "BOTTOMRIGHT", 0, 0)

    local scroll = holder.scrollFrame
    local scrollChild = holder.scrollChild

    local box = CreateFrame("EditBox", nil, scrollChild)
    box:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", 4, -4)
    box:SetMultiLine(true)
    box:SetMaxLetters(0)
    box:SetAutoFocus(false)
    UI.SetFont(box, UI.FONT_SIZE_NORMAL, C.text)
    box:SetTextInsets(0, 0, 0, 0)
    box:SetScript("OnEscapePressed", function(self) self:ClearFocus() end)

    local measure = box:CreateFontString(nil, "OVERLAY")
    UI.SetFont(measure, UI.FONT_SIZE_NORMAL, C.text)
    measure:Hide()

    local function UpdateLayout()
        local scrollW = scroll:GetWidth()
        if scrollW and scrollW > 24 then
            local textW = scrollW - 12
            box:SetWidth(textW)
            measure:SetWidth(textW - 4)
            scrollChild:SetWidth(scrollW)
        end

        local text = box:GetText() or " "
        measure:SetText(text)
        local textHeight = measure:GetStringHeight() or 14
        local visibleH = scroll:GetHeight()
        local contentH = math.max(textHeight + 16, visibleH)
        box:SetHeight(contentH)
        scrollChild:SetHeight(contentH)
        scroll:SetVerticalScroll(0)
        if holder.UpdateThumb then holder:UpdateThumb() end
    end

    box:SetScript("OnTextChanged", function()
        UpdateLayout()
    end)

    scroll:SetScript("OnSizeChanged", function()
        UpdateLayout()
    end)

    holder.editBox = box
    holder.UpdateLayout = UpdateLayout
    return holder
end

local TAB_ICON_SIZE = 16

local function NormalizeTabEntry(entry)
    if type(entry) == "table" then
        return entry.text or entry.name or "", entry.icon
    end
    return tostring(entry), nil
end

function UI.Tab(name, icon)
    return { text = name, icon = icon }
end

function UI.CreateTabBar(parent, tabNames, opts)
    opts = opts or {}
    local bar = CreateFrame("Frame", nil, parent)
    bar:SetHeight(28)
    bar:SetPoint("TOPLEFT", 0, 0)
    bar:SetPoint("TOPRIGHT", 0, 0)

    local sep = bar:CreateTexture(nil, "ARTWORK")
    sep:SetTexture(FLAT)
    sep:SetVertexColor(unpack(C.border))
    sep:SetPoint("BOTTOMLEFT", 0, 0)
    sep:SetPoint("BOTTOMRIGHT", 0, 0)
    sep:SetHeight(1)

    local tabs = {}
    local prev

    local function SelectTab(index, fireCallback)
        for i, t in ipairs(tabs) do
            t.underline:Hide()
            UI.SetFont(t.label, UI.FONT_SIZE_NORMAL, C.textDim)
            if t.icon then
                if i == index then
                    t.icon:SetVertexColor(1, 1, 1)
                else
                    t.icon:SetVertexColor(0.55, 0.55, 0.55)
                end
            end
        end
        local tab = tabs[index]
        if not tab then return end
        tab.underline:Show()
        UI.SetFont(tab.label, UI.FONT_SIZE_NORMAL, C.text)
        if tab.icon then tab.icon:SetVertexColor(1, 1, 1) end
        bar.selectedIndex = index
        if fireCallback ~= false and bar.OnTabSelected then
            local label = NormalizeTabEntry(tabNames[index])
            bar.OnTabSelected(index, label)
        end
    end

    for i, entry in ipairs(tabNames) do
        local name, iconPath = NormalizeTabEntry(entry)
        local tab = CreateFrame("Button", nil, bar)
        tab:SetHeight(28)
        local label = tab:CreateFontString(nil, "OVERLAY")
        UI.SetFont(label, UI.FONT_SIZE_NORMAL, C.textDim)
        label:SetText(name)

        local leftPad = 6
        if iconPath then
            local icon = tab:CreateTexture(nil, "ARTWORK")
            icon:SetSize(TAB_ICON_SIZE, TAB_ICON_SIZE)
            icon:SetTexture(iconPath)
            icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
            icon:SetPoint("LEFT", tab, "LEFT", leftPad, 0)
            icon:SetVertexColor(0.55, 0.55, 0.55)
            label:SetPoint("LEFT", icon, "RIGHT", 4, 0)
            tab.icon = icon
            leftPad = leftPad + TAB_ICON_SIZE + 4
        else
            label:SetPoint("CENTER")
        end

        tab:SetWidth(leftPad + label:GetStringWidth() + 10)

        local underline = tab:CreateTexture(nil, "ARTWORK")
        underline:SetTexture(FLAT)
        underline:SetVertexColor(unpack(C.accent))
        underline:SetPoint("BOTTOMLEFT", 0, 0)
        underline:SetPoint("BOTTOMRIGHT", 0, 0)
        underline:SetHeight(2)
        underline:Hide()

        if prev then
            tab:SetPoint("LEFT", prev, "RIGHT", 12, 0)
        else
            tab:SetPoint("LEFT", 0, 0)
        end
        prev = tab

        tab.label = label
        tab.underline = underline
        tab:SetScript("OnClick", function()
            SelectTab(i, true)
        end)

        tabs[i] = tab
    end

    bar.tabs = tabs
    function bar:SelectTab(index, fireCallback)
        SelectTab(index, fireCallback)
    end

    if opts.autoSelectFirst ~= false and tabs[1] then
        SelectTab(1, false)
    end

    return bar
end

function UI.CreateNavBar(parent, opts)
    opts = opts or {}
    local height = opts.height or 36

    local bar = CreateFrame("Frame", nil, parent)
    bar:SetHeight(height)
    bar:SetPoint("TOPLEFT", parent, "TOPLEFT", 0, 0)
    bar:SetPoint("TOPRIGHT", parent, "TOPRIGHT", 0, 0)
    UI.Fill(bar, C.bgHeader)

    local sep = bar:CreateTexture(nil, "ARTWORK")
    sep:SetTexture(FLAT)
    sep:SetVertexColor(unpack(C.border))
    sep:SetPoint("BOTTOMLEFT", 0, 0)
    sep:SetPoint("BOTTOMRIGHT", 0, 0)
    sep:SetHeight(1)

    local itemFrame = CreateFrame("Frame", nil, bar)
    itemFrame:SetPoint("TOPLEFT", 10, 0)
    itemFrame:SetPoint("BOTTOMLEFT", 10, 0)
    itemFrame:SetWidth(1)

    local actionFrame = CreateFrame("Frame", nil, bar)
    actionFrame:SetPoint("TOPRIGHT", -8, 0)
    actionFrame:SetPoint("BOTTOMRIGHT", -8, 0)
    actionFrame:SetWidth(1)

    local itemBtns = {}
    local prev

    local function SelectItem(id, fireCallback)
        bar.selectedId = id
        for itemId, btn in pairs(itemBtns) do
            if itemId == id then
                btn.underline:Show()
                UI.SetFont(btn.label, UI.FONT_SIZE_NORMAL, C.text)
            else
                btn.underline:Hide()
                UI.SetFont(btn.label, UI.FONT_SIZE_NORMAL, C.textDim)
            end
        end
        if fireCallback ~= false and bar.OnItemSelected then
            bar.OnItemSelected(id)
        end
    end

    for _, item in ipairs(opts.items or {}) do
        local btn = CreateFrame("Button", nil, itemFrame)
        btn:SetHeight(height)
        local label = btn:CreateFontString(nil, "OVERLAY")
        UI.SetFont(label, UI.FONT_SIZE_NORMAL, C.textDim)
        label:SetPoint("CENTER")
        label:SetText(item.text or item.id)
        btn:SetWidth(label:GetStringWidth() + 24)

        local underline = btn:CreateTexture(nil, "ARTWORK")
        underline:SetTexture(FLAT)
        underline:SetVertexColor(unpack(C.accent))
        underline:SetPoint("BOTTOMLEFT", 4, 2)
        underline:SetPoint("BOTTOMRIGHT", -4, 2)
        underline:SetHeight(2)
        underline:Hide()

        if prev then
            btn:SetPoint("LEFT", prev, "RIGHT", 4, 0)
        else
            btn:SetPoint("LEFT", 0, 0)
        end
        prev = btn

        btn.label = label
        btn.underline = underline
        btn:SetScript("OnClick", function()
            SelectItem(item.id, true)
        end)
        itemBtns[item.id] = btn
    end

    if prev then
        itemFrame:SetWidth(prev:GetRight() - itemFrame:GetLeft())
    end

    bar.actions = {}
    local nextAction
    for i = #opts.actions, 1, -1 do
        local action = opts.actions[i]
        local btnOpts = {}
        if action.primary then
            btnOpts.bg = { C.accent[1] * 0.25, C.accent[2] * 0.25, C.accent[3] * 0.25, 1 }
            btnOpts.textColor = C.accent
            btnOpts.hoverBorder = C.accent
        end
        local btn = UI.CreateButton(actionFrame, action.width or 88, 26, action.text, btnOpts)
        if nextAction then
            btn:SetPoint("RIGHT", nextAction, "LEFT", -6, 0)
        else
            btn:SetPoint("RIGHT", actionFrame, "RIGHT", 0, 0)
        end
        btn:SetPoint("TOP", actionFrame, "TOP", 0, -5)
        if action.onClick then
            btn:SetScript("OnClick", action.onClick)
        end
        nextAction = btn
        if action.id then bar.actions[action.id] = btn end
    end

    if nextAction then
        actionFrame:SetWidth(nextAction:GetLeft() - actionFrame:GetLeft() + nextAction:GetWidth())
    end

    bar.itemBtns = itemBtns
    function bar:SelectItem(id, fireCallback)
        SelectItem(id, fireCallback)
    end
    function bar:GetSelected()
        return self.selectedId
    end

    return bar
end


local function EnsureDropdownCloser()
    if UI._dropdownCloser then return end
    local closer = CreateFrame("Frame", "EbonholdHubDropdownCloser", UIParent)
    closer:SetAllPoints(UIParent)
    closer:SetFrameStrata("FULLSCREEN_DIALOG")
    closer:SetFrameLevel(90)
    closer:EnableMouse(true)
    closer:Hide()
    closer:SetScript("OnMouseDown", function()
        UI.CloseAllDropdowns()
    end)
    UI._dropdownCloser = closer
end

function UI.CloseAllDropdowns()
    if UI._openDropdowns then
        for i = #UI._openDropdowns, 1, -1 do
            local dd = UI._openDropdowns[i]
            if dd and dd.list then dd.list:Hide() end
        end
    end
    if UI._dropdownCloser then UI._dropdownCloser:Hide() end
end

function UI.CloseDropdown(dropdown)
    if dropdown and dropdown.list then dropdown.list:Hide() end
    if UI._dropdownCloser then UI._dropdownCloser:Hide() end
end

function UI.CreateDropdown(parent, width, options, defaultIndex)
    EnsureDropdownCloser()
    local dropdown = CreateFrame("Button", nil, parent)
    dropdown:SetSize(width, 24)
    dropdown.options = options

    UI.Fill(dropdown, C.elementBg)
    local border = UI.AddThinBorder(dropdown, C.border, 1)

    local label = dropdown:CreateFontString(nil, "OVERLAY")
    UI.SetFont(label, UI.FONT_SIZE_NORMAL, C.text)
    label:SetPoint("LEFT", 8, 0)
    label:SetPoint("RIGHT", -20, 0)
    label:SetJustifyH("LEFT")

    local arrow = dropdown:CreateFontString(nil, "OVERLAY")
    UI.SetFont(arrow, UI.FONT_SIZE_SMALL, C.textDim)
    arrow:SetPoint("RIGHT", -8, 0)
    arrow:SetText("v")

    dropdown.selectedIndex = defaultIndex or 1
    label:SetText(options[dropdown.selectedIndex] and options[dropdown.selectedIndex].text or "")

    local list = CreateFrame("Frame", nil, UIParent)
    list:SetWidth(width)
    list:SetHeight(math.max(22, #options * 22))
    list:SetFrameStrata("FULLSCREEN_DIALOG")
    list:SetFrameLevel(100)
    list:EnableMouse(true)
    UI.Fill(list, C.elementBg)
    UI.AddThinBorder(list, C.border, 1)
    list:Hide()

    for i, opt in ipairs(options) do
        local btn = CreateFrame("Button", nil, list)
        btn:SetHeight(22)
        btn:SetPoint("TOPLEFT", list, "TOPLEFT", 0, -(i - 1) * 22)
        btn:SetPoint("TOPRIGHT", list, "TOPRIGHT", 0, -(i - 1) * 22)

        local hoverBg = UI.Fill(btn, C.elementBg)
        local optLabel = btn:CreateFontString(nil, "OVERLAY")
        UI.SetFont(optLabel, UI.FONT_SIZE_NORMAL, C.text)
        optLabel:SetPoint("LEFT", 8, 0)
        optLabel:SetText(opt.text)

        btn:SetScript("OnEnter", function() hoverBg:SetVertexColor(unpack(C.elementHov)) end)
        btn:SetScript("OnLeave", function() hoverBg:SetVertexColor(unpack(C.elementBg)) end)
        btn:SetScript("OnClick", function()
            dropdown.selectedIndex = i
            label:SetText(opt.text)
            list:Hide()
            if UI._dropdownCloser then UI._dropdownCloser:Hide() end
            if dropdown.OnValueChanged then
                dropdown.OnValueChanged(opt.value, opt.text, i)
            end
        end)
    end

    local function PositionList()
        list:SetWidth(width)
        list:SetHeight(math.max(22, #options * 22))
        local x = dropdown:GetLeft()
        local y = dropdown:GetBottom()
        if x and y then
            list:ClearAllPoints()
            list:SetPoint("TOPLEFT", UIParent, "BOTTOMLEFT", x, y - 2)
        end
    end

    dropdown:SetScript("OnClick", function()
        if list:IsShown() then
            list:Hide()
            if UI._dropdownCloser then UI._dropdownCloser:Hide() end
        else
            UI.CloseAllDropdowns()
            PositionList()
            list:Show()
            if UI._dropdownCloser then UI._dropdownCloser:Show() end
        end
    end)
    dropdown:SetScript("OnEnter", function()
        border.top:SetVertexColor(unpack(C.accent))
        border.bottom:SetVertexColor(unpack(C.accent))
        border.left:SetVertexColor(unpack(C.accent))
        border.right:SetVertexColor(unpack(C.accent))
    end)
    dropdown:SetScript("OnLeave", function()
        border.top:SetVertexColor(unpack(C.border))
        border.bottom:SetVertexColor(unpack(C.border))
        border.left:SetVertexColor(unpack(C.border))
        border.right:SetVertexColor(unpack(C.border))
    end)

    dropdown:SetScript("OnHide", function()
        list:Hide()
    end)

    function dropdown:SetSelectedIndex(index)
        local opt = options[index]
        if not opt then return end
        self.selectedIndex = index
        label:SetText(opt.text)
    end

    function dropdown:GetSelectedValue()
        local opt = options[self.selectedIndex]
        return opt and opt.value
    end

    function dropdown:Release()
        list:Hide()
        list:SetParent(nil)
        if UI._openDropdowns then
            for i, dd in ipairs(UI._openDropdowns) do
                if dd == dropdown then
                    table.remove(UI._openDropdowns, i)
                    break
                end
            end
        end
    end

    dropdown.list = list
    dropdown.label = label
    UI._openDropdowns = UI._openDropdowns or {}
    UI._openDropdowns[#UI._openDropdowns + 1] = dropdown
    return dropdown
end

function UI.CreateScrollFrame(parent, width, height)
    local holder = CreateFrame("Frame", nil, parent)
    if width and height then holder:SetSize(width, height) end
    UI.Fill(holder, C.elementBg)
    UI.AddThinBorder(holder, C.border, 1)

    local scroll = CreateFrame("ScrollFrame", nil, holder)
    scroll:SetPoint("TOPLEFT", 4, -4)
    scroll:SetPoint("BOTTOMRIGHT", -14, 4)

    local scrollChild = CreateFrame("Frame", nil, scroll)
    scrollChild:SetSize((width or 100) - 18, 1)
    scroll:SetScrollChild(scrollChild)

    local track = CreateFrame("Frame", nil, holder)
    track:SetPoint("TOPRIGHT", -3, -4)
    track:SetPoint("BOTTOMRIGHT", -3, 4)
    track:SetWidth(6)
    UI.Fill(track, C.border)

    local thumb = CreateFrame("Frame", nil, track)
    thumb:SetWidth(6)
    thumb:SetPoint("TOP", 0, 0)
    UI.Fill(thumb, C.accent)
    thumb:EnableMouse(true)

    local function UpdateThumb()
        local range = scrollChild:GetHeight() - scroll:GetHeight()
        if range <= 0 then
            thumb:SetHeight(track:GetHeight())
            thumb:SetPoint("TOP", 0, 0)
            return
        end
        local trackH = track:GetHeight()
        local thumbH = math.max(20, trackH * (scroll:GetHeight() / scrollChild:GetHeight()))
        thumb:SetHeight(thumbH)
        local scrollPos = scroll:GetVerticalScroll()
        local offset = (scrollPos / range) * (trackH - thumbH)
        thumb:SetPoint("TOP", 0, -offset)
    end

    scroll:EnableMouseWheel(true)
    scroll:SetScript("OnMouseWheel", function(self, delta)
        local range = scrollChild:GetHeight() - self:GetHeight()
        if range <= 0 then return end
        local newPos = self:GetVerticalScroll() - delta * 30
        newPos = math.max(0, math.min(range, newPos))
        self:SetVerticalScroll(newPos)
        UpdateThumb()
    end)

    scroll:SetScript("OnScrollRangeChanged", function()
        UpdateThumb()
    end)

    holder.scrollFrame = scroll
    holder.scrollChild = scrollChild
    holder.UpdateThumb = UpdateThumb
    return holder
end

function UI.CreateProgressBar(parent, width, height, minVal, maxVal, defaultVal)
    local bar = CreateFrame("Frame", nil, parent)
    bar:SetSize(width, height)

    UI.Fill(bar, C.elementBg)
    UI.AddThinBorder(bar, C.border, 1)

    local fill = bar:CreateTexture(nil, "ARTWORK")
    fill:SetTexture(FLAT)
    fill:SetVertexColor(unpack(C.accent))
    fill:SetPoint("TOPLEFT", 1, -1)
    fill:SetPoint("BOTTOMLEFT", 1, 1)

    local valueText = bar:CreateFontString(nil, "OVERLAY")
    UI.SetFont(valueText, UI.FONT_SIZE_SMALL, C.text)
    valueText:SetPoint("CENTER")

    bar.min = minVal or 0
    bar.max = maxVal or 100

    function bar:SetProgress(value)
        value = math.max(self.min, math.min(self.max, value))
        local pct = (value - self.min) / (self.max - self.min)
        local fullWidth = self:GetWidth() - 2
        fill:SetWidth(math.max(1, fullWidth * pct))
        valueText:SetText(math.floor(pct * 100) .. "%")
    end

    bar:SetProgress(defaultVal or minVal or 0)
    bar.fill = fill
    bar.valueText = valueText
    return bar
end

local QUALITY_BORDER = {
    [0] = { 1.0, 1.0, 1.0 },
    [1] = { 30/255, 1.0, 0.0 },
    [2] = { 0.0, 112/255, 221/255 },
    [3] = { 163/255, 53/255, 238/255 },
    [4] = { 1.0, 128/255, 0.0 },
}

local function ApplyIconButtonBorder(btn, color)
    if not btn or not btn._border then return end
    btn._border.top:SetVertexColor(unpack(color))
    btn._border.bottom:SetVertexColor(unpack(color))
    btn._border.left:SetVertexColor(unpack(color))
    btn._border.right:SetVertexColor(unpack(color))
end

function UI.SetIconButtonQuality(btn, quality)
    if not btn then return end
    quality = quality or 0
    btn._quality = quality
    btn._qualityColor = QUALITY_BORDER[quality] or QUALITY_BORDER[0]
    ApplyIconButtonBorder(btn, btn._qualityColor)
end

function UI.ClearIconButtonQuality(btn)
    if not btn then return end
    btn._quality = nil
    btn._qualityColor = nil
    ApplyIconButtonBorder(btn, C.border)
end

function UI.CreateIconButton(parent, size)
    local btn = CreateFrame("Button", nil, parent)
    btn:SetSize(size, size)
    local bg = UI.Fill(btn, C.elementBg)
    local border = UI.AddThinBorder(btn, C.border, 1)

    local icon = btn:CreateTexture(nil, "ARTWORK")
    icon:SetPoint("TOPLEFT", 2, -2)
    icon:SetPoint("BOTTOMRIGHT", -2, 2)
    icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)

    btn:SetScript("OnEnter", function()
        bg:SetVertexColor(unpack(C.elementHov))
        border.top:SetVertexColor(unpack(C.accent))
        border.bottom:SetVertexColor(unpack(C.accent))
        border.left:SetVertexColor(unpack(C.accent))
        border.right:SetVertexColor(unpack(C.accent))
    end)
    btn:SetScript("OnLeave", function()
        bg:SetVertexColor(unpack(C.elementBg))
        ApplyIconButtonBorder(btn, btn._qualityColor or C.border)
    end)

    btn.icon = icon
    btn._bg = bg
    btn._border = border
    return btn
end

function UI.OpenExternalURL(url)
    if type(url) ~= "string" or url == "" then return false end
    if not url:match("^https?://") then return false end
    if type(EbonholdOpenURL) == "function" then
        EbonholdOpenURL(url)
        return true
    end
    return false
end

function EbonholdHub.OpenExternalURL(url)
    return UI.OpenExternalURL(url)
end
