-- ============================================================================
-- Vamoose's Companion - UI Framework
-- Factory methods for UI components (auto-register with ThemeEngine)
-- ============================================================================

VC = VC or {}
VC.UI = {}

local WHITE = "Interface\\Buttons\\WHITE8x8"
local SHADOW_TEX = "Interface\\AddOns\\VamoosesCompanion\\textures\\vc_shadow"

local PI = math.pi

-- ============================================================================
-- SHARED MODEL SCENE HELPERS
-- ============================================================================

-- Create a ModelScene with standard camera + lighting (Narcissus pattern)
function VC.UI:CreateModelScene(parent, camZ)
    local scene = CreateFrame("ModelScene", nil, parent, "NoCameraControlModelSceneMixinTemplate")
    scene:SetCameraPosition(4, 0, camZ or 0.2)
    scene:SetCameraOrientationByYawPitchRoll(PI, 0, 0)
    scene:SetCameraFieldOfView(0.75)
    scene:SetLightDiffuseColor(0.8, 0.8, 0.8)
    scene:SetLightAmbientColor(0.6, 0.6, 0.6)
    scene:SetLightPosition(1, 0, 1)
    scene:SetLightDirection(-1, 0, -1)
    return scene
end

-- Auto-fit: scale actor to fill viewport using bounding box projection
function VC.UI:AutoFitActor(scene, actor, scaleMult)
    local a, b, c, d, e, f2 = actor:GetActiveBoundingBox()
    if not a then return end
    local height = f2 - c
    local width = e - b
    local effectiveScale = scene:GetEffectiveScale() / UIParent:GetEffectiveScale()
    local sceneW, sceneH = scene:GetSize()
    sceneW = sceneW * effectiveScale
    sceneH = sceneH * effectiveScale
    local hw, hh = width / 2, height / 2
    local x1, y1 = scene:Project3DPointTo2D(0, -hw, -hh)
    local x2, y2 = scene:Project3DPointTo2D(0, -hw, hh)
    local x3, y3 = scene:Project3DPointTo2D(0, hw, -hh)
    local x4, y4 = scene:Project3DPointTo2D(0, hw, hh)
    if x1 and x2 and x3 and x4 then
        local minX = math.min(x1, x2, x3, x4)
        local maxX = math.max(x1, x2, x3, x4)
        local minY = math.min(y1, y2, y3, y4)
        local maxY = math.max(y1, y2, y3, y4)
        local spanX = maxX - minX
        local spanY = maxY - minY
        local scale = math.min(0.8 * sceneH / spanY, 0.8 * sceneW / spanX)
        actor:SetScale(scale * (scaleMult or 1))
    end
end

-- Unified model pipeline: Hide -> Callback -> Load -> Show (Blizzard canonical pattern)
-- opts: { scale, posY, posZ, yaw, animID, onLoaded }
function VC.UI:SetCreatureOnScene(scene, displayID, opts)
    opts = opts or {}
    -- Destroy previous actor to flush internal engine state (reuse causes stale rendering)
    if scene.actor then
        scene.actor:SetOnModelLoadedCallback(nil)  -- cancel in-flight load callback
        scene.actor:ClearModel()
        scene.actor:Hide()
        scene.actor = nil
    end
    local actor = scene:CreateActor(nil, "ModelSceneActorTemplate")
    scene.actor = actor
    actor:Hide()
    scene._pendingDisplayID = displayID
    local sm = opts.scale or 0.5
    actor:SetOnModelLoadedCallback(function(a)
        if scene._pendingDisplayID ~= displayID then return end
        a:SetUseCenterForOrigin(false, false, true)
        a:SetPosition(0, opts.posY or 0, opts.posZ or 0)
        a:SetYaw(opts.yaw or 0)
        a:SetAnimation(opts.animID or 0)
        VC.UI:AutoFitActor(scene, a, sm)
        a:Show()
        if opts.onLoaded then opts.onLoaded(a) end
    end)
    actor:SetModelByCreatureDisplayID(displayID)
    return actor
end

-- Apply portrait frame style (atlas or ornate texture)
local ORNATE_TEX = "Interface\\AddOns\\VamoosesCompanion\\textures\\vc_portrait_ornate"
local ORNATE_COORDS = { 0.240, 0.160, 0.760, 0.840 } -- 266x348 centered in 512x512

function VC.UI:ApplyPortraitFrameStyle(f)
    local style = VC.Store:GetState().config.portraitFrame or "atlas"
    local isOrnate = (style == "ornate")
    local isMinimal = (style == "minimal")
    -- Background texture
    if isMinimal then
        f.bgTex:SetTexture(nil)
        f.bgTex:SetAtlas(nil)
    elseif isOrnate then
        f.bgTex:SetAtlas(nil)
        f.bgTex:SetTexture(ORNATE_TEX)
        f.bgTex:SetTexCoord(ORNATE_COORDS[1], ORNATE_COORDS[3], ORNATE_COORDS[2], ORNATE_COORDS[4])
    else
        f.bgTex:SetTexCoord(0, 1, 0, 1)
        f.bgTex:SetTexture(nil)
        f.bgTex:SetAtlas("barbershop-background")
    end
    -- Bond pips: hidden in minimal, nudged inward for ornate
    if f.bondPips then
        local pipX = isOrnate and 12 or 10
        for _, pip in ipairs(f.bondPips) do
            pip:SetShown(not isMinimal)
            pip:ClearAllPoints()
            pip:SetPoint("CENTER", f, "TOPLEFT", pipX, pip._yOff)
        end
    end
    -- Mood bar: hidden in minimal, nudged for ornate
    if f._moodGrad then
        f._moodGrad:SetShown(not isMinimal)
        local moodX = isOrnate and -5 or -3
        local moodY = isOrnate and 34 or 33
        f._moodGrad:ClearAllPoints()
        f._moodGrad:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", moodX, moodY)
    end
end

-- ============================================================================
-- PORTRAIT FRAME (ModelScene) - supports "compact" and "standard" sizes
-- ============================================================================

function VC.UI:CreatePortraitFrame(parent)
    local state = VC.Store:GetState()
    local sizeMode = (state.config.portraitSize or VC.Constants.PORTRAIT_SIZE_DEFAULT)
    local isStandard = (sizeMode ~= "compact")

    local W = isStandard and VC.Constants.DISPLAY.PORTRAIT_SIZE_STANDARD_W or VC.Constants.DISPLAY.PORTRAIT_SIZE
    local H = isStandard and VC.Constants.DISPLAY.PORTRAIT_SIZE_STANDARD_H or VC.Constants.DISPLAY.PORTRAIT_SIZE

    local f = CreateFrame("Frame", "VC_PortraitFrame", parent or UIParent)
    f:SetSize(W, H)
    f:SetFrameStrata("MEDIUM")
    f:SetMovable(true)
    f:SetClampedToScreen(true)
    f:EnableMouse(true)
    f:RegisterForDrag("LeftButton")
    f._isStandard = isStandard

    -- Portrait frame background (swappable: atlas or ornate)
    local bgTex = f:CreateTexture(nil, "BACKGROUND")
    bgTex:SetAllPoints()
    f.bgTex = bgTex

    -- Drag handlers
    f:SetScript("OnDragStart", function(self) self:StartMoving() end)
    f:SetScript("OnDragStop", function(self)
        self:StopMovingOrSizing()
        local point, _, relativePoint, x, y = self:GetPoint()
        VC.Store:Dispatch("SET_PORTRAIT_POSITION", {
            point = { point = point, relTo = "UIParent", relPoint = relativePoint, x = x, y = y }
        })
        -- Update companion facing to look toward player
        if VC.Portrait and VC.Portrait.UpdateFacingForPosition then
            local companion = VC.Portrait:GetActiveCompanion()
            local actor = self.scene and self.scene.actor
            if actor and companion then
                VC.Portrait:UpdateFacingForPosition(actor, companion)
            end
        end
    end)

    -- 3D ModelScene (viewport bleeds past border for immersive framing)
    local scene = VC.UI:CreateModelScene(f, 0.2)
    scene:SetSize(W + 180, H + 110)
    scene:SetPoint("TOPLEFT", -90, 100)
    scene:SetFrameLevel(f:GetFrameLevel() + 1)
    scene:EnableMouse(false)
    f.scene = scene
    f.model = nil  -- set to actor after first model load

    -- Bonding pips (left silver bar, bottom-to-top)
    local NUM_BOND_PIPS = 5
    local PIP_SIZE = 8
    local pipBottomY = -(H - 44)
    local pipSpacing = 16
    f.bondPips = {}
    for i = 1, NUM_BOND_PIPS do
        local pip = f:CreateTexture(nil, "OVERLAY")
        pip:SetSize(PIP_SIZE, PIP_SIZE)
        local yOff = pipBottomY + (i - 1) * pipSpacing
        pip:SetPoint("CENTER", f, "TOPLEFT", 10, yOff)
        pip:SetAtlas("WhiteCircle-RaidBlips")
        pip:SetVertexColor(0.2, 0.2, 0.2, 0.4)
        pip._yOff = yOff
        f.bondPips[i] = pip
    end

    -- =========================================================================
    -- Mood bar: thin gradient on right silver bar, sine-wave height drift
    -- =========================================================================
    local GRAD_TEX = "Interface\\AddOns\\VamoosesCompanion\\textures\\vc_gradient_v"
    local mc = VC.Constants.MOOD_COLORS.NEUTRAL
    local barH = H - 60 -- usable height on the silver bar

    local moodFrame = CreateFrame("Frame", nil, f)
    moodFrame:SetAllPoints()
    moodFrame:SetFrameLevel(scene:GetFrameLevel() - 1)
    local moodBar = moodFrame:CreateTexture(nil, "OVERLAY")
    moodBar:SetTexture(GRAD_TEX)
    moodBar:SetWidth(10)
    moodBar:SetHeight(barH)
    moodBar:SetPoint("BOTTOMRIGHT", moodFrame, "BOTTOMRIGHT", -3, 33)
    moodBar:SetBlendMode("ADD")
    moodBar:SetVertexColor(mc.r, mc.g, mc.b, 1.0)
    f._moodGrad = moodBar


    local DRIFT_SPEED = 0.4
    local _driftElapsed = 0
    f:SetScript("OnUpdate", function(self, elapsed)
        if not self:IsShown() then return end
        if InCombatLockdown() then return end
        _driftElapsed = _driftElapsed + elapsed
        if _driftElapsed < 0.05 then return end
        _driftElapsed = 0
        local stretch = 0.6 + math.sin(GetTime() * DRIFT_SPEED) * 0.4
        moodBar:SetHeight(barH * stretch)
    end)


    -- Update mood bar on mood changes (real-time)
    if not VC.UI._moodRegistered then
        VC.EventBus:Register("VC_MOOD_CHANGED", function()
            VC.UI:UpdatePortraitMood()
        end)
        VC.UI._moodRegistered = true
    end
    if not VC.UI._portraitFrames then VC.UI._portraitFrames = {} end
    VC.UI._portraitFrames[#VC.UI._portraitFrames + 1] = f

    -- Apply frame style after all elements are created (pips, mood bar need to exist)
    VC.UI:ApplyPortraitFrameStyle(f)

    return f
end

-- ============================================================================
-- PIP UPDATES (mood + bonding)
-- ============================================================================

-- Update mood wash gradient color on all portrait frames
function VC.UI:UpdatePortraitMood()
    if not self._portraitFrames then return end
    if InCombatLockdown() then return end
    local mood = VC.Mood and VC.Mood:GetMood() or "NEUTRAL"
    local mc = VC.Constants.MOOD_COLORS[mood] or VC.Constants.MOOD_COLORS.NEUTRAL
    for _, f in ipairs(self._portraitFrames) do
        if f and f._moodGrad and f:IsShown() then
            f._moodGrad:SetVertexColor(mc.r, mc.g, mc.b, 1.0)
        end
    end
end

-- Update bonding pips on a portrait frame (lights up bottom-to-top)
function VC.UI:RefreshPortraitBonding(f, activeKey)
    if not f or not f.bondPips then return end
    if InCombatLockdown() then return end
    if not VC.Bonding then return end

    local levels = VC.Constants.BONDING.LEVELS
    local info = VC.Bonding:GetLevelInfo(activeKey)
    if not info then return end

    -- Find current level index (1-5)
    local levelIndex = 1
    for i, lvl in ipairs(levels) do
        if info.rapport >= lvl.threshold then levelIndex = i end
    end

    local c = VC.Theme:GetScheme()
    local glowR = c.bonding_glow and c.bonding_glow.r or 0.302
    local glowG = c.bonding_glow and c.bonding_glow.g or 0.788
    local glowB = c.bonding_glow and c.bonding_glow.b or 0.965

    local n = #f.bondPips
    for i, pip in ipairs(f.bondPips) do
        if i <= levelIndex then
            -- Top pip (n) is brightest, bottom pip (1) is dimmest
            local t = n > 1 and (i - 1) / (n - 1) or 1 -- 0 at bottom, 1 at top
            local alpha = 0.4 + 0.6 * t
            pip:SetVertexColor(glowR, glowG, glowB, alpha)
        else
            pip:SetVertexColor(0.2, 0.2, 0.2, 0.4)
        end
    end
end

-- Refresh companion name label on a portrait frame
function VC.UI:RefreshPortraitName(f, companion)
    if not f or not f.nameLabel then return end
    local name = companion and companion.name or ""
    f.nameLabel:SetText(name)
end

-- ============================================================================
-- BUTTON
-- ============================================================================

function VC.UI:CreateButton(parent, text, width, height, onClick)
    local scheme = VC.Theme:GetScheme()
    local btn = CreateFrame("Button", nil, parent)
    btn:SetSize(width, height)

    local bg = btn:CreateTexture(nil, "BACKGROUND")
    bg:SetAllPoints()
    bg:SetTexture(WHITE)
    bg:SetVertexColor(scheme.button_normal.r, scheme.button_normal.g, scheme.button_normal.b, scheme.button_normal.a)
    btn._bg = bg

    local fs = btn:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    fs:SetPoint("CENTER")
    fs:SetText(text)
    fs:SetTextColor(scheme.button_text_norm.r, scheme.button_text_norm.g, scheme.button_text_norm.b, scheme.button_text_norm.a)
    btn:SetFontString(fs)

    btn:SetScript("OnClick", onClick)
    btn:SetScript("OnEnter", function(self)
        local s = self._scheme or scheme
        self._bg:SetVertexColor(s.button_hover.r, s.button_hover.g, s.button_hover.b, s.button_hover.a)
        local f = self:GetFontString()
        if f then f:SetTextColor(s.button_text_hover.r, s.button_text_hover.g, s.button_text_hover.b, s.button_text_hover.a) end
    end)
    btn:SetScript("OnLeave", function(self)
        local s = self._scheme or scheme
        local col = self._activeColor or s.button_normal
        self._bg:SetVertexColor(col.r, col.g, col.b, col.a)
        local f = self:GetFontString()
        if f then f:SetTextColor(s.button_text_norm.r, s.button_text_norm.g, s.button_text_norm.b, s.button_text_norm.a) end
    end)

    VC.Theme:Register(btn, "Button")
    return btn
end

-- ============================================================================
-- PILL BUTTON (companion selector style)
-- ============================================================================

function VC.UI:CreatePillButton(parent, text, width, height, onClick)
    local c = VC.Theme:GetScheme()
    local btn = CreateFrame("Button", nil, parent, "BackdropTemplate")
    btn:SetSize(width, height)
    btn:SetBackdrop(VC.Theme.BACKDROP_INSET)
    btn:SetBackdropColor(c.panel.r, c.panel.g, c.panel.b, 1)
    btn:SetBackdropBorderColor(c.border.r, c.border.g, c.border.b, 1)

    local fs = btn:CreateFontString(nil, "OVERLAY")
    fs:SetPoint("CENTER")
    fs:SetFont("Fonts\\ARIALN.TTF", 11, "")
    fs:SetText(text)
    fs:SetTextColor(c.text_dim.r, c.text_dim.g, c.text_dim.b, 1)
    btn:SetFontString(fs)

    btn:SetScript("OnClick", onClick)
    btn:SetScript("OnEnter", function(self)
        if not self._activeColor then
            local s = self._scheme or c
            self:SetBackdropColor(s.panel_alt and s.panel_alt.r or s.button_hover.r,
                                  s.panel_alt and s.panel_alt.g or s.button_hover.g,
                                  s.panel_alt and s.panel_alt.b or s.button_hover.b, 1)
        end
    end)
    btn:SetScript("OnLeave", function(self)
        local s = self._scheme or c
        if self._activeColor then
            self:SetBackdropColor(s.accent.r, s.accent.g, s.accent.b, 0.20)
        else
            self:SetBackdropColor(s.panel.r, s.panel.g, s.panel.b, 1)
        end
    end)

    VC.Theme:Register(btn, "PillButton")
    return btn
end

-- Set active/inactive state on a pill button
function VC.UI:SetPillActive(btn, active)
    local wasActive = btn._activeColor
    btn._activeColor = active and true or nil
    local c = btn._scheme or VC.Theme:GetScheme()
    if active then
        btn:SetBackdropColor(c.accent.r, c.accent.g, c.accent.b, 0.20)
        btn:SetBackdropBorderColor(c.accent.r, c.accent.g, c.accent.b, 1)
        local fs = btn:GetFontString()
        if fs then fs:SetTextColor(c.text_header.r, c.text_header.g, c.text_header.b, 1) end
        -- Flash only on NEWLY activated pill (not on every refresh)
        if not wasActive then
            if not btn._flashOverlay then
                local ov = btn:CreateTexture(nil, "OVERLAY")
                ov:SetAllPoints()
                ov:SetTexture("Interface\\Buttons\\WHITE8x8")
                ov:SetVertexColor(1, 1, 1, 1)
                ov:Hide()
                local ag = btn:CreateAnimationGroup()
                local anim = ag:CreateAnimation("Alpha")
                anim:SetFromAlpha(0.3)
                anim:SetToAlpha(0)
                anim:SetDuration(0.2)
                ag:SetScript("OnFinished", function() ov:Hide() end)
                btn._flashOverlay = ov
                btn._flashAG = ag
            end
            btn._flashOverlay:SetAlpha(0.3)
            btn._flashOverlay:Show()
            if btn._flashAG:IsPlaying() then btn._flashAG:Stop() end
            btn._flashAG:Play()
        end
    else
        btn:SetBackdropColor(c.panel.r, c.panel.g, c.panel.b, 1)
        btn:SetBackdropBorderColor(c.border.r, c.border.g, c.border.b, 1)
        local fs = btn:GetFontString()
        if fs then fs:SetTextColor(c.text_dim.r, c.text_dim.g, c.text_dim.b, 1) end
    end
end

-- ============================================================================
-- TAB BUTTON
-- ============================================================================

function VC.UI:CreateTabButton(parent, text, onClick)
    local c = VC.Theme:GetScheme()
    local btn = CreateFrame("Button", nil, parent)
    btn:SetSize(80, 28)

    local fs = btn:CreateFontString(nil, "OVERLAY")
    fs:SetAllPoints()
    fs:SetFont("Fonts\\ARIALN.TTF", 11, "")
    fs:SetText(text)
    fs:SetTextColor(c.text_dim.r, c.text_dim.g, c.text_dim.b, 1)
    btn:SetFontString(fs)

    -- 2px underline at bottom
    local underline = btn:CreateTexture(nil, "ARTWORK")
    underline:SetHeight(2)
    underline:SetPoint("BOTTOMLEFT", 0, 0)
    underline:SetPoint("BOTTOMRIGHT", 0, 0)
    underline:SetTexture(WHITE)
    underline:SetVertexColor(c.accent.r, c.accent.g, c.accent.b, 1)
    underline:Hide()
    btn._underline = underline
    btn._active = false

    btn:SetScript("OnClick", onClick)
    btn:SetScript("OnEnter", function(self)
        if not self._active then
            local s = self._scheme or c
            local label = self:GetFontString()
            if label then label:SetTextColor(s.text_header.r, s.text_header.g, s.text_header.b, 1) end
        end
    end)
    btn:SetScript("OnLeave", function(self)
        if not self._active then
            local s = self._scheme or c
            local label = self:GetFontString()
            if label then label:SetTextColor(s.text_dim.r, s.text_dim.g, s.text_dim.b, 1) end
        end
    end)

    VC.Theme:Register(btn, "TabButton")
    return btn
end

function VC.UI:SetTabActive(btn, active)
    btn._active = active
    local c = btn._scheme or VC.Theme:GetScheme()
    local fs = btn:GetFontString()
    if fs then
        if active then
            fs:SetTextColor(c.text_header.r, c.text_header.g, c.text_header.b, 1)
        else
            fs:SetTextColor(c.text_dim.r, c.text_dim.g, c.text_dim.b, 1)
        end
    end
    if btn._underline then
        btn._underline:SetShown(active)
    end
end

-- ============================================================================
-- SEGMENTED BUTTON GROUP (chattiness replacement for slider)
-- ============================================================================

-- options = { { key="minimal", label="Minimal" }, ... }
-- returns container frame. Call container:SetValue(key) to activate a segment.
function VC.UI:CreateSegmentedGroup(parent, options, totalWidth, height, onChange)
    height = height or 24
    local c = VC.Theme:GetScheme()
    local n = #options
    local gap = 2
    local btnW = math.floor((totalWidth - (n - 1) * gap) / n)

    local container = CreateFrame("Frame", nil, parent)
    container:SetSize(totalWidth, height)
    container._buttons = {}
    container._activeKey = nil

    for i, opt in ipairs(options) do
        local btn = CreateFrame("Button", nil, container, "BackdropTemplate")
        btn:SetSize(btnW, height)
        btn:SetPoint("TOPLEFT", (i - 1) * (btnW + gap), 0)
        btn:SetBackdrop(VC.Theme.BACKDROP_INSET)
        btn:SetBackdropColor(c.panel.r, c.panel.g, c.panel.b, 1)
        btn:SetBackdropBorderColor(c.border.r, c.border.g, c.border.b, 1)

        local fs = btn:CreateFontString(nil, "OVERLAY")
        fs:SetAllPoints()
        fs:SetFont("Fonts\\ARIALN.TTF", 11, "")
        fs:SetText(opt.label)
        fs:SetTextColor(c.text_dim.r, c.text_dim.g, c.text_dim.b, 1)
        btn:SetFontString(fs)
        btn._segKey = opt.key

        btn:SetScript("OnClick", function(self)
            container:SetValue(self._segKey)
            if onChange then onChange(self._segKey) end
        end)

        -- SetActive method on each button
        btn.SetActive = function(self, active)
            local s = self._scheme or VC.Theme:GetScheme()
            if active then
                self:SetBackdropColor(s.accent.r, s.accent.g, s.accent.b, 0.15)
                self:SetBackdropBorderColor(s.accent.r, s.accent.g, s.accent.b, 1)
                local label = self:GetFontString()
                if label then label:SetTextColor(s.text_header.r, s.text_header.g, s.text_header.b, 1) end
            else
                self:SetBackdropColor(s.panel.r, s.panel.g, s.panel.b, 1)
                self:SetBackdropBorderColor(s.border.r, s.border.g, s.border.b, 1)
                local label = self:GetFontString()
                if label then label:SetTextColor(s.text_dim.r, s.text_dim.g, s.text_dim.b, 1) end
            end
        end

        VC.Theme:Register(btn, "SegmentedButton")
        container._buttons[opt.key] = btn
    end

    container.SetValue = function(self, key)
        self._activeKey = key
        for k, btn in pairs(self._buttons) do
            btn:SetActive(k == key)
        end
    end

    return container
end

-- ============================================================================
-- RADIO GROUP (HDG GoblinTab style: circle + dot atlas)
-- ============================================================================

local RADIO_SIZE = 12

function VC.UI:CreateRadioGroup(parent, options, onChange)
    local c = VC.Theme:GetScheme()

    local container = CreateFrame("Frame", nil, parent)
    container:SetSize(300, 20)
    container._buttons = {}
    container._activeKey = nil

    local prevRadio = nil
    for _, opt in ipairs(options) do
        local radio = CreateFrame("Button", nil, container)
        radio:SetSize(RADIO_SIZE + 4 + 50, 16)
        if prevRadio then
            radio:SetPoint("LEFT", prevRadio, "RIGHT", 6, 0)
        else
            radio:SetPoint("LEFT", 0, 0)
        end

        radio.circle = radio:CreateTexture(nil, "ARTWORK")
        radio.circle:SetAtlas("common-radiobutton-circle")
        radio.circle:SetSize(RADIO_SIZE, RADIO_SIZE)
        radio.circle:SetPoint("LEFT", 0, 0)

        radio.dot = radio:CreateTexture(nil, "OVERLAY")
        radio.dot:SetAtlas("common-radiobutton-dot")
        radio.dot:SetSize(RADIO_SIZE, RADIO_SIZE)
        radio.dot:SetPoint("CENTER", radio.circle, "CENTER", 0, 0)
        radio.dot:Hide()

        radio.label = radio:CreateFontString(nil, "OVERLAY")
        radio.label:SetPoint("LEFT", radio.circle, "RIGHT", 3, 0)
        radio.label:SetFont("Fonts\\ARIALN.TTF", 13, "")
        radio.label:SetText(opt.label)
        radio._radioKey = opt.key

        -- Auto-size hit area to label width
        radio:SetScript("OnShow", function(self)
            local textW = self.label:GetStringWidth() or 30
            self:SetWidth(RADIO_SIZE + 3 + textW + 4)
        end)

        radio:SetScript("OnClick", function()
            container:SetValue(opt.key)
            if onChange then onChange(opt.key) end
        end)

        container._buttons[opt.key] = radio
        prevRadio = radio
    end

    -- Measure total width after last button
    container:SetScript("OnShow", function(self)
        local totalW = 0
        for _, btn in pairs(self._buttons) do
            if btn:IsShown() then
                local textW = btn.label:GetStringWidth() or 30
                btn:SetWidth(RADIO_SIZE + 3 + textW + 4)
            end
        end
    end)

    container.SetValue = function(self, key)
        self._activeKey = key
        local s = VC.Theme:GetScheme()
        for k, radio in pairs(self._buttons) do
            if k == key then
                radio.dot:Show()
                radio.circle:SetDesaturated(false)
                radio.circle:SetVertexColor(s.accent.r, s.accent.g, s.accent.b)
                radio.dot:SetVertexColor(s.accent.r, s.accent.g, s.accent.b)
                radio.label:SetTextColor(s.text_header.r, s.text_header.g, s.text_header.b, 1)
            else
                radio.dot:Hide()
                radio.circle:SetDesaturated(false)
                radio.circle:SetVertexColor(s.text_dim.r, s.text_dim.g, s.text_dim.b)
                radio.label:SetTextColor(s.text_dim.r, s.text_dim.g, s.text_dim.b, 1)
            end
        end
    end

    return container
end

-- ============================================================================
-- CHECKBOX
-- ============================================================================

function VC.UI:CreateCheckbox(parent, text, onChange)
    local f = CreateFrame("Frame", nil, parent)
    f:SetSize(160, 22)

    local cb = CreateFrame("CheckButton", nil, f, "UICheckButtonTemplate")
    cb:SetSize(22, 22)
    cb:SetPoint("LEFT", 0, 0)
    cb:SetScript("OnClick", function(self)
        if onChange then onChange(self:GetChecked()) end
    end)
    f.checkbox = cb

    local label = f:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    label:SetPoint("LEFT", cb, "RIGHT", 4, 0)
    label:SetText(text)
    f.label = label

    VC.Theme:Register(f, "Checkbox")
    return f
end

-- ============================================================================
-- SECTION LABEL
-- ============================================================================

function VC.UI:CreateSectionLabel(parent, text)
    local f = CreateFrame("Frame", nil, parent)
    f:SetSize(320, 20)

    local label = f:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    label:SetPoint("LEFT", 0, 0)
    label:SetText(text)
    label.isHeader = true
    f.label = label

    local line = f:CreateTexture(nil, "ARTWORK")
    line:SetHeight(1)
    line:SetPoint("LEFT", label, "RIGHT", 6, 0)
    line:SetPoint("RIGHT", f, "RIGHT", 0, 0)
    local scheme = VC.Theme:GetScheme()
    line:SetColorTexture(scheme.accent.r, scheme.accent.g, scheme.accent.b, 0.3)
    f.line = line

    VC.Theme:Register(f, "SectionLabel")
    return f
end

-- Create a 1px separator line (auto-registered with ThemeEngine Separator skinner)
function VC.UI:CreateSeparator(parent)
    local sep = parent:CreateTexture(nil, "ARTWORK")
    sep:SetHeight(1)
    sep:SetTexture("Interface\\Buttons\\WHITE8x8")
    VC.Theme:Register(sep, "Separator")
    return sep
end

-- ============================================================================
-- SCROLLBAR STYLING (slim accent-colored, hides Blizzard chrome)
-- ============================================================================

function VC.UI.StyleScrollBar(scrollFrame)
    if not scrollFrame then return end

    local scheme = VC.Theme:GetScheme()
    local tr, tg, tb = 0, 0.4, 0.4
    if scheme and scheme.accent then
        tr, tg, tb = scheme.accent.r, scheme.accent.g, scheme.accent.b
    end

    local scrollBar = scrollFrame.ScrollBar or _G[scrollFrame:GetName() and scrollFrame:GetName() .. "ScrollBar" or ""]
    if not scrollBar then return end

    -- Hide all default Blizzard artwork
    for _, region in ipairs({scrollBar:GetRegions()}) do
        if region:GetObjectType() == "Texture" then
            region:SetAlpha(0)
        end
    end

    -- Hide up/down buttons
    local scrollUpButton = scrollBar.ScrollUpButton or (scrollBar:GetName() and _G[scrollBar:GetName() .. "ScrollUpButton"])
    local scrollDownButton = scrollBar.ScrollDownButton or (scrollBar:GetName() and _G[scrollBar:GetName() .. "ScrollDownButton"])

    if scrollUpButton then
        scrollUpButton:SetAlpha(0)
        scrollUpButton:EnableMouse(false)
        scrollUpButton:SetSize(1, 1)
    end
    if scrollDownButton then
        scrollDownButton:SetAlpha(0)
        scrollDownButton:EnableMouse(false)
        scrollDownButton:SetSize(1, 1)
    end

    -- Slim dark track
    scrollBar:SetWidth(6)

    if not scrollBar.trackBg then
        local track = scrollBar:CreateTexture(nil, "BACKGROUND")
        track:SetTexture(WHITE)
        track:SetVertexColor(0.1, 0.1, 0.1, 0.3)
        track:SetAllPoints(scrollBar)
        scrollBar.trackBg = track
    end

    -- Minimal thumb
    local thumbTexture = scrollBar.ThumbTexture or scrollBar:GetThumbTexture()
    if thumbTexture then
        thumbTexture:SetAlpha(1)
        thumbTexture:SetTexture(WHITE)
        thumbTexture:SetVertexColor(tr, tg, tb, 0.7)
        thumbTexture:SetSize(4, 40)
    end

    if not scrollBar._vcHooked then
        scrollBar._vcHooked = true
        scrollBar:HookScript("OnEnter", function()
            if thumbTexture then thumbTexture:SetVertexColor(tr + 0.1, tg + 0.1, tb + 0.1, 0.9) end
        end)
        scrollBar:HookScript("OnLeave", function()
            if thumbTexture then thumbTexture:SetVertexColor(tr, tg, tb, 0.7) end
        end)
    end

    scrollBar._vcThumbTexture = thumbTexture
    scrollBar._vcTrackBg = scrollBar.trackBg
    VC.Theme:Register(scrollBar, "ScrollBar")
end

-- ============================================================================
-- SCROLL FRAME
-- ============================================================================

function VC.UI:CreateScrollFrame(parent, width, height)
    local sf = CreateFrame("ScrollFrame", nil, parent, "UIPanelScrollFrameTemplate")
    sf:SetSize(width, height)

    local content = CreateFrame("Frame", nil, sf)
    content:SetSize(width - 24, 1) -- height set dynamically
    sf:SetScrollChild(content)

    return sf, content
end
