EbonholdHub.SettingsView = {}

local UI = EbonholdHub.UI
local C  = UI.Colors
local ST = EbonholdHub.SiteTheme
local SW = EbonholdHub.SiteWidgets
local SC = ST and ST.C
local SL = ST and ST.Layout

local QUALITY_LABELS = {
    [0] = { name = "Common",    color = "ffffff" },
    [1] = { name = "Uncommon",  color = "19ff19" },
    [2] = { name = "Rare",      color = "0066ff" },
    [3] = { name = "Epic",      color = "cc66ff" },
    [4] = { name = "Legendary", color = "ff8000" },
}

local FAMILY_ORDER = {
    "Tank", "Survivability", "Healer", "Caster", "Melee", "Ranged", "No family",
}
local WHITELIST_ROW1 = { "Tank", "Survivability", "Healer", "Caster" }
local WHITELIST_ROW2 = { "Melee", "Ranged", "No family" }

-- Auto Pick Style level labels shown next to the slider
local AGGRESSION_LABELS = {
    [1] = { label = "Pick Only",       color = { 0.4, 0.9, 0.4 },
            desc = "Just picks the best echo. Never rerolls or freezes." },
    [2] = { label = "Light Touch",     color = { 0.6, 0.85, 0.5 },
            desc = "Banishes F-tier trash only. Saves rerolls and freezes." },
    [3] = { label = "Normal",          color = { 0.9, 0.85, 0.4 },
            desc = "Rerolls weak offers, banishes F trash, freezes runner-ups." },
    [4] = { label = "Smart (Best)",    color = { 1.0, 0.65, 0.3 },
            desc = "Recommended default. Rerolls first on bad offers. Banishes F and duplicates. Uses banish instead of reroll when out of rerolls." },
    [5] = { label = "Perfectionist",   color = { 1.0, 0.35, 0.25 },
            desc = "Same as Smart, but keeps rerolling until the offer is strong (A or S tier)." },
}

local HELP_TEXT = table.concat({
    "Auto Pick reacts when the game offers three echoes.",
    "",
    "What it does (in order):",
    "  1. If a build locked echo is offered → always pick it (top priority).",
    "  2. Banish F-tier trash.",
    "  3. Reroll weak offers first (if rerolls remain).",
    "  4. Banish duplicates — and weak echoes only when out of rerolls.",
    "  5. Freeze a runner-up when the offer is good.",
    "  6. Pick the best echo.",
    "",
    "Tiers are set in the Echoes tab.",
    "  S — always want. A — really want. B — nice to have.",
    "  C — unassigned default. F — never pick.",
    "",
    "Auto Pick Style:",
    "  1 Pick Only      — just picks, no rerolls or freezes.",
    "  2 Light Touch    — banishes F trash only.",
    "  3 Normal         — balanced; fewer banish tricks than Smart.",
    "  4 Smart (Best)   — recommended default; plays like manual.",
    "  5 Perfectionist  — same, but stricter about echo quality.",
    "",
    "Max picks per echo — set x1/x2/x3/x∞ per echo in the Echoes tab (click the badge).",
    "",
    "Banish Protection — checked families are never auto-banished.",
    "F tier echoes are always banished when offered.",
}, "\n")

local HEADER_PAD  = 6
local TOOLBAR_H   = 36   -- slim toolbar (matches other tabs)
local TITLE_ROW_H = 0    -- title removed
local GAP = 8
local SECTION_GAP = 8
local HEADER_BLOCK_H = 34
local CHECK_ROW_H = 28
local FAMILY_COL_W = 118
local BAN_LIST_H = 120
local ICON_SIZE = 28
local ICON_GAP = 4
local ICON_STEP = ICON_SIZE + ICON_GAP
local BAN_LIST_PAD = 4

local AGGR_ROW_H   = 58   -- height of each style card
local WHITELIST_H  = HEADER_BLOCK_H + (CHECK_ROW_H * 2) + 14 + 28 + 8
local AGGRESSION_H = 8 + 16 + 4 + 14 + 10 + 5 * AGGR_ROW_H + 8   -- header+hint+cards
local TIMING_H     = HEADER_BLOCK_H + 250 + 8
local TIMING_SLIDER_W = 240
local SHOW_BANISH_PROTECTION = false

local GLOBAL_SETTINGS = {
    {
        key = "evalDelay",
        label = "Action delay",
        flavor = "Pause before auto pick uses banish, reroll, freeze, or select.",
        warning = "Very low values may cause the addon to malfunction.",
        min = 0.1, max = 3.0, step = 0.1, default = 2,
        format = function(v) return string.format("%.1fs", v) end,
    },
    {
        key = "toastDuration",
        label = "Toast duration",
        flavor = "How long auto pick result notifications stay on screen.",
        min = 0.1, max = 3.0, step = 0.1, default = 3,
        format = function(v) return string.format("%.1fs", v) end,
    },
    {
        key = "toastChargesOpacity",
        label = "Toast opacity",
        flavor = "Transparency of the auto pick result toast (icons, header, and charges). Lower = more see-through.",
        min = 0.15, max = 1.0, step = 0.05, default = 0.92,
        format = function(v) return string.format("%d%%", math.floor(v * 100 + 0.5)) end,
    },
}

local QUALITY_BORDER_COLORS = {
    [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 viewFrame
local headerPanel, helpSection, helpBtn, helpVisible
local bodyFrame, bodyScroll, bodyScrollChild
local whitelistRows = {}
local whitelistWarningLabel
local aggressionLevelRows = {}
local currentAggressionLevel = 4
local timingSliders = {}
local timingPinRow
local RefreshInputs  -- forward declaration

local function PlaceWrapLabel(label, parent, topAnchor, topRelPoint, topX, topY, width)
    label:SetPoint("TOPLEFT", topAnchor, topRelPoint, topX, topY)
    label:SetWidth(width or 500)
    label:SetJustifyH("LEFT")
    label:SetWordWrap(true)
end

local function SaveGlobalSetting(key, value)
    EbonholdHubDB.globalSettings = EbonholdHubDB.globalSettings or {}
    EbonholdHubDB.globalSettings[key] = value
    if key == "toastChargesOpacity" and EbonholdHub.Toast and EbonholdHub.Toast.RefreshStyle then
        EbonholdHub.Toast.RefreshStyle()
    end
end

-- ── Header layout ─────────────────────────────────────────────────────────────

local function LayoutHeader()
    if not headerPanel or not helpBtn then return end
    local root = headerPanel:GetParent()
    if not root then return end

    local helpH = 0
    if helpVisible then
        helpSection:Show()
        helpSection:ClearAllPoints()
        helpSection:SetPoint("TOPLEFT",  headerPanel, "TOPLEFT",  HEADER_PAD, -TOOLBAR_H - GAP)
        helpSection:SetPoint("TOPRIGHT", headerPanel, "TOPRIGHT", -HEADER_PAD, 0)
        local helpLabel = helpSection._label
        if helpLabel then
            helpH = math.max(120, helpLabel:GetStringHeight() + 4)
            helpSection:SetHeight(helpH)
        end
    else
        helpSection:Hide()
    end

    local totalH = TOOLBAR_H + (helpVisible and (GAP + helpH + GAP) or 0)

    headerPanel:ClearAllPoints()
    headerPanel:SetHeight(totalH)
    headerPanel:SetPoint("TOPLEFT",  root, "TOPLEFT",  0, 0)
    headerPanel:SetPoint("TOPRIGHT", root, "TOPRIGHT", 0, 0)

    helpBtn:ClearAllPoints()
    helpBtn:SetPoint("RIGHT",  headerPanel, "RIGHT",  -HEADER_PAD, 0)
    helpBtn:SetPoint("TOP",    headerPanel, "TOP",    0, -(TOOLBAR_H - 24) / 2)
end

local function BodyContentHeight()
    local h = AGGRESSION_H + SECTION_GAP + TIMING_H
    if SHOW_BANISH_PROTECTION then
        h = h + SECTION_GAP + WHITELIST_H
    end
    return h
end

local function LayoutBody()
    if not bodyScrollChild then return end
    bodyScrollChild:SetHeight(BodyContentHeight())
    if bodyScroll and bodyScroll.UpdateThumb then
        bodyScroll.UpdateThumb()
    end
end

local function ToggleHelp()
    helpVisible = not helpVisible
    if helpBtn then
        helpBtn._label:SetText(helpVisible and "Hide help" or "How it works")
    end
    LayoutHeader()
    LayoutBody()
end

local function RefreshHeader() end  -- title removed; kept for compatibility

-- ── Section helpers ───────────────────────────────────────────────────────────

local function RefreshWhitelistToggles()
    local settings = EbonholdHub.BuildForm.GetEditingSettings()
    settings.banishFamilyWhitelist = settings.banishFamilyWhitelist or {}
    local allSelected = true
    for _, fam in ipairs(FAMILY_ORDER) do
        local row = whitelistRows[fam]
        if row then
            local selected = settings.banishFamilyWhitelist[fam] or false
            row:SetChecked(selected)
            if not selected then allSelected = false end
        end
    end
    if whitelistWarningLabel then
        if allSelected then whitelistWarningLabel:Show()
        else whitelistWarningLabel:Hide() end
    end
end

local function SetWhitelistFamily(family, checked)
    local settings = EbonholdHub.BuildForm.GetEditingSettings()
    settings.banishFamilyWhitelist = settings.banishFamilyWhitelist or {}
    settings.banishFamilyWhitelist[family] = checked and true or nil
    RefreshWhitelistToggles()
end

-- ── Panel builders ────────────────────────────────────────────────────────────

local function ApplyAggressionRowStyle(row, level, selected)
    local meta = AGGRESSION_LABELS[level]
    if not meta then return end
    if selected then
        row._bg:SetVertexColor(unpack(SC.accentBg12))
        SW.SetBorderColor(row._border, SC.accentBorder35)
        row._stripe:Show()
        row._numBg:SetVertexColor(meta.color[1], meta.color[2], meta.color[3], 1)
        row._numLabel:SetTextColor(SC.bg[1], SC.bg[2], SC.bg[3])
        row._nameLabel:SetTextColor(unpack(SC.text))
        SW.SetFont(row._nameLabel, 12, "semibold")
    else
        row._bg:SetVertexColor(unpack(SC.elementBg))
        SW.SetBorderColor(row._border, SC.border)
        row._stripe:Hide()
        row._numBg:SetVertexColor(unpack(SC.bgElevated))
        row._numLabel:SetTextColor(unpack(SC.textMuted))
        row._nameLabel:SetTextColor(unpack(SC.textDim))
        SW.SetFont(row._nameLabel, 12, "medium")
    end
end

local function SetAggressionLevel(level, save)
    currentAggressionLevel = level
    for i, row in ipairs(aggressionLevelRows) do
        ApplyAggressionRowStyle(row, i, i == level)
    end
    if save then
        local settings = EbonholdHub.BuildForm.GetEditingSettings()
        settings.aggressionLevel = level
    end
end

local function PopulateAggressionPanel(panel)
    local PAD = HEADER_PAD + 2
    local CARD_PAD = 8

    local header = SW.Label(panel, "Auto Pick Style", 12, SC.text, false, "semibold")
    header:SetPoint("TOPLEFT", panel, "TOPLEFT", PAD, -PAD)

    local hint = SW.Label(panel,
        "How closely auto pick mimics manual play. Higher = tries harder for better echoes.",
        11, SC.textDim)
    hint:SetPoint("TOPLEFT", header, "BOTTOMLEFT", 0, -3)
    hint:SetPoint("TOPRIGHT", panel, "TOPRIGHT", -PAD, 0)
    hint:SetJustifyH("LEFT")

    aggressionLevelRows = {}
    local prevRow = nil

    for level = 1, 5 do
        local meta = AGGRESSION_LABELS[level]
        local row = CreateFrame("Button", nil, panel)
        row:SetHeight(AGGR_ROW_H)
        row:SetPoint("LEFT",  panel, "LEFT",  0, 0)
        row:SetPoint("RIGHT", panel, "RIGHT", 0, 0)
        if prevRow then
            row:SetPoint("TOP", prevRow, "BOTTOM", 0, -2)
        else
            row:SetPoint("TOP", hint, "BOTTOM", 0, -10)
        end

        row._bg     = SW.Fill(row, SC.elementBg)
        row._border = SW.ThinBorder(row, SC.border, 1)

        -- Left accent stripe (shown when selected)
        local stripe = row:CreateTexture(nil, "ARTWORK")
        stripe:SetTexture(ST.FLAT)
        stripe:SetVertexColor(meta.color[1], meta.color[2], meta.color[3], 1)
        stripe:SetWidth(3)
        stripe:SetPoint("TOPLEFT",    row, "TOPLEFT",    0, 0)
        stripe:SetPoint("BOTTOMLEFT", row, "BOTTOMLEFT", 0, 0)
        stripe:Hide()
        row._stripe = stripe

        -- Number badge
        local numBadge = CreateFrame("Frame", nil, row)
        numBadge:SetSize(24, 24)
        numBadge:SetPoint("TOPLEFT", row, "TOPLEFT", CARD_PAD + 4, -(AGGR_ROW_H - 24) / 2)
        row._numBg = SW.Fill(numBadge, SC.bgElevated)
        SW.ThinBorder(numBadge, SC.border, 1)
        local numLbl = SW.Label(numBadge, tostring(level), 11, SC.textMuted, false, "semibold")
        numLbl:SetPoint("CENTER")
        row._numLabel = numLbl

        -- Name
        local nameLbl = SW.Label(row, meta.label, 12, SC.textDim, false, "medium")
        nameLbl:SetPoint("TOPLEFT", numBadge, "TOPRIGHT", 10, 1)
        row._nameLabel = nameLbl

        -- Description
        local descLbl = SW.Label(row, meta.desc, 10, SC.textMuted)
        descLbl:SetPoint("TOPLEFT",  nameLbl, "BOTTOMLEFT",  0, -2)
        descLbl:SetPoint("TOPRIGHT", row,     "TOPRIGHT", -CARD_PAD, 0)
        descLbl:SetJustifyH("LEFT")
        descLbl:SetWordWrap(true)
        row._descLabel = descLbl

        -- Hover
        row:SetScript("OnEnter", function(self)
            if currentAggressionLevel ~= level then
                self._bg:SetVertexColor(unpack(SC.bgElevated))
            end
        end)
        row:SetScript("OnLeave", function(self)
            if currentAggressionLevel ~= level then
                self._bg:SetVertexColor(unpack(SC.elementBg))
            end
        end)

        row:SetScript("OnClick", function()
            SetAggressionLevel(level, true)
        end)

        aggressionLevelRows[level] = row
        prevRow = row
    end
end

local function PopulateWhitelistPanel(panel)
    local PAD = HEADER_PAD + 2
    local header = SW.Label(panel, "Banish Protection", 12, SC.text, false, "semibold")
    header:SetPoint("TOPLEFT", panel, "TOPLEFT", PAD, -PAD)

    local hint = SW.Label(panel,
        "Checked = this family is safe from auto-banish.",
        11, SC.textDim)
    hint:SetPoint("TOPLEFT", header, "BOTTOMLEFT", 0, -2)
    hint:SetPoint("TOPRIGHT", panel, "TOPRIGHT", -PAD, 0)
    hint:SetJustifyH("LEFT")

    local function AddRow(families, labelY)
        for i, fam in ipairs(families) do
            local famKey = fam
            local cx = PAD + (i - 1) * FAMILY_COL_W
            local row = UI.CreateCheckbox(panel, famKey, false)
            row:SetPoint("TOPLEFT", panel, "TOPLEFT", cx, labelY)
            row:SetWidth(FAMILY_COL_W - 4)
            row.OnValueChanged = function(checked) SetWhitelistFamily(famKey, checked) end
            whitelistRows[famKey] = row
        end
    end

    AddRow(WHITELIST_ROW1, -38)
    AddRow(WHITELIST_ROW2, -38 - CHECK_ROW_H)

    whitelistWarningLabel = SW.Label(panel,
        "|cffff6666All families protected — auto-banish will not remove any echo.|r",
        11, SC.textDim)
    whitelistWarningLabel:SetPoint("TOPLEFT", panel, "TOPLEFT", PAD, -38 - CHECK_ROW_H * 2 - 4)
    whitelistWarningLabel:SetPoint("TOPRIGHT", panel, "TOPRIGHT", -PAD, 0)
    whitelistWarningLabel:SetJustifyH("LEFT")
    whitelistWarningLabel:Hide()
end

local function CreateGlobalSettingRow(panel, anchor, entry)
    local PAD = HEADER_PAD + 2
    local lbl = SW.Label(panel, entry.label, 12, SC.text)
    lbl:SetPoint("TOPLEFT", anchor, "BOTTOMLEFT", 0, -12)

    local flavor = SW.Label(panel, entry.flavor, 11, SC.textDim)
    PlaceWrapLabel(flavor, panel, lbl, "BOTTOMLEFT", 0, -2)

    local sliderAnchor = flavor
    if entry.warning then
        local warn = SW.Label(panel, entry.warning, 11, C.danger)
        PlaceWrapLabel(warn, panel, flavor, "BOTTOMLEFT", 0, -2)
        sliderAnchor = warn
    end

    local slider = UI.CreateSlider(panel, TIMING_SLIDER_W, entry.min, entry.max, entry.default, {
        step = entry.step,
        format = function() return "" end,
    })
    slider:SetPoint("TOPLEFT", sliderAnchor, "BOTTOMLEFT", 0, -8)
    if slider.valueText then slider.valueText:Hide() end

    local valText = SW.Label(panel, entry.format(entry.default), 11, SC.text)
    valText:SetPoint("LEFT", slider, "RIGHT", 8, 0)

    slider.OnValueChanged = function(_, value)
        valText:SetText(entry.format(value))
        SaveGlobalSetting(entry.key, value)
    end

    return slider
end

local function PopulateTimingPanel(panel)
    local PAD = HEADER_PAD + 2
    local header = SW.Label(panel, "Timing", 12, SC.text, false, "semibold")
    header:SetPoint("TOPLEFT", panel, "TOPLEFT", PAD, -PAD)

    local hint = SW.Label(panel,
        "Controls how quickly auto pick reacts and how long result toasts remain visible. Applies to all characters on this account.",
        11, SC.textDim)
    hint:SetPoint("TOPLEFT", header, "BOTTOMLEFT", 0, -3)
    hint:SetPoint("TOPRIGHT", panel, "TOPRIGHT", -PAD, 0)
    hint:SetJustifyH("LEFT")

    local rowAnchor = hint
    timingSliders = {}
    for _, entry in ipairs(GLOBAL_SETTINGS) do
        timingSliders[entry.key] = CreateGlobalSettingRow(panel, rowAnchor, entry)
        rowAnchor = timingSliders[entry.key]
    end

    timingPinRow = UI.CreateCheckbox(panel, "Keep auto pick toast visible until dismissed", false)
    timingPinRow:SetPoint("TOPLEFT", rowAnchor, "BOTTOMLEFT", 0, -14)
    timingPinRow:SetPoint("TOPRIGHT", panel, "TOPRIGHT", -8, 0)
    timingPinRow.OnValueChanged = function(checked)
        EbonholdHubDB.globalSettings = EbonholdHubDB.globalSettings or {}
        EbonholdHubDB.globalSettings.toastPinned = checked and true or false
        if EbonholdHub.Toast and EbonholdHub.Toast.SetPinned then
            EbonholdHub.Toast.SetPinned(checked)
        end
    end
end

-- ── Body scroll ───────────────────────────────────────────────────────────────

local function CreateBodyScroll(parent)
    local holder = CreateFrame("Frame", nil, parent)
    holder:SetPoint("TOPLEFT", parent, "TOPLEFT", 0, 0)
    holder:SetPoint("BOTTOMRIGHT", parent, "BOTTOMRIGHT", 0, 0)

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

    local child = CreateFrame("Frame", nil, scroll)
    child:SetWidth(1)
    scroll:SetScrollChild(child)

    local track = CreateFrame("Frame", nil, holder)
    track:SetPoint("TOPRIGHT",    holder, "TOPRIGHT",    -2, 0)
    track:SetPoint("BOTTOMRIGHT", holder, "BOTTOMRIGHT", -2, 0)
    track:SetWidth(4)
    SW.Fill(track, SC.borderSoft)

    local thumb = CreateFrame("Frame", nil, track)
    thumb:SetWidth(4)
    thumb:SetPoint("TOP", track, "TOP", 0, 0)
    SW.Fill(thumb, SC.accent)
    thumb:EnableMouse(true)

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

    scroll:EnableMouseWheel(true)
    scroll:SetScript("OnMouseWheel", function(self, delta)
        local range = child: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", UpdateThumb)
    scroll:SetScript("OnSizeChanged", function()
        local w = scroll:GetWidth()
        if w and w > 0 then child:SetWidth(math.max(1, w)) end
        UpdateThumb()
    end)

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

-- ── Refresh ───────────────────────────────────────────────────────────────────

RefreshInputs = function()
    local settings = EbonholdHub.BuildForm.GetEditingSettings()

    -- Aggression level cards
    local level = math.max(1, math.min(5, math.floor(settings.aggressionLevel or 4)))
    SetAggressionLevel(level, false)

    -- Whitelist + skip repeats
    RefreshWhitelistToggles()

    -- Global timing sliders
    local gs = EbonholdHubDB.globalSettings or {}
    for _, entry in ipairs(GLOBAL_SETTINGS) do
        local slider = timingSliders[entry.key]
        if slider then
            slider:SetValue(gs[entry.key] or entry.default)
        end
    end
    if timingPinRow then
        timingPinRow:SetChecked(gs.toastPinned == true)
    end
end

-- ── Build layout ──────────────────────────────────────────────────────────────

local function MakeSectionPanel(parent)
    local p = CreateFrame("Frame", nil, parent)
    SW.Fill(p, SC.statCellBg)
    SW.ThinBorder(p, SC.border, 1)
    return p
end

local function BuildViewFrame(parent)
    local f = CreateFrame("Frame", nil, parent)

    -- ── Toolbar (slim, sidebarBg, matching other tabs) ─────────────────────
    headerPanel = CreateFrame("Frame", nil, f)
    SW.Fill(headerPanel, SC.sidebarBg)
    local toolbarSep = headerPanel:CreateTexture(nil, "ARTWORK")
    toolbarSep:SetTexture(ST.FLAT)
    toolbarSep:SetVertexColor(unpack(SC.border))
    toolbarSep:SetHeight(1)
    toolbarSep:SetPoint("BOTTOMLEFT",  headerPanel, "BOTTOMLEFT",  0, 0)
    toolbarSep:SetPoint("BOTTOMRIGHT", headerPanel, "BOTTOMRIGHT", 0, 0)

    helpSection = CreateFrame("Frame", nil, headerPanel)
    helpSection:Hide()
    local helpLabel = SW.Label(helpSection, HELP_TEXT, 11, SC.textDim)
    helpLabel:SetPoint("TOPLEFT",  helpSection, "TOPLEFT",  0, 0)
    helpLabel:SetPoint("TOPRIGHT", helpSection, "TOPRIGHT", 0, 0)
    helpLabel:SetJustifyH("LEFT")
    helpSection._label = helpLabel

    helpBtn = SW.CreateOutlineButton(headerPanel, "How it works", 100)
    helpBtn:SetHeight(24)
    helpBtn:SetScript("OnClick", ToggleHelp)

    bodyFrame = CreateFrame("Frame", nil, f)
    bodyFrame:SetPoint("TOPLEFT",    headerPanel, "BOTTOMLEFT",  0, -GAP)
    bodyFrame:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", 0, 0)

    bodyScroll, bodyScrollChild = CreateBodyScroll(bodyFrame)

    local aggrPanel = MakeSectionPanel(bodyScrollChild)
    aggrPanel:SetPoint("TOPLEFT",  bodyScrollChild, "TOPLEFT",  0, 0)
    aggrPanel:SetPoint("TOPRIGHT", bodyScrollChild, "TOPRIGHT", 0, 0)
    aggrPanel:SetHeight(AGGRESSION_H)
    PopulateAggressionPanel(aggrPanel)

    local sectionAnchor = aggrPanel
    if SHOW_BANISH_PROTECTION then
        local whitelistPanel = MakeSectionPanel(bodyScrollChild)
        whitelistPanel:SetPoint("TOPLEFT",  aggrPanel, "BOTTOMLEFT",  0, -SECTION_GAP)
        whitelistPanel:SetPoint("TOPRIGHT", aggrPanel, "BOTTOMRIGHT", 0, -SECTION_GAP)
        whitelistPanel:SetHeight(WHITELIST_H)
        PopulateWhitelistPanel(whitelistPanel)
        sectionAnchor = whitelistPanel
    end

    local timingPanel = MakeSectionPanel(bodyScrollChild)
    timingPanel:SetPoint("TOPLEFT",  sectionAnchor, "BOTTOMLEFT",  0, -SECTION_GAP)
    timingPanel:SetPoint("TOPRIGHT", sectionAnchor, "BOTTOMRIGHT", 0, -SECTION_GAP)
    timingPanel:SetHeight(TIMING_H)
    PopulateTimingPanel(timingPanel)

    helpVisible = false
    f._layoutV5 = true
    LayoutHeader()
    LayoutBody()
    return f
end

-- ── State reset ───────────────────────────────────────────────────────────────

local function ResetView()
    viewFrame = nil
    bodyFrame = nil
    bodyScroll = nil
    bodyScrollChild = nil
    whitelistRows = {}
    whitelistWarningLabel = nil
    aggressionLevelRows = {}
    currentAggressionLevel = 4
    timingSliders = {}
    timingPinRow = nil
end

local function EnsureBuilt(container)
    if viewFrame and viewFrame._layoutV5 then return end
    ResetView()
    viewFrame = BuildViewFrame(container)
end

-- ── Public ────────────────────────────────────────────────────────────────────

function EbonholdHub.SettingsView.Mount(container)
    EnsureBuilt(container)
    viewFrame:SetParent(container)
    viewFrame:ClearAllPoints()
    viewFrame:SetAllPoints(container)
    RefreshHeader()
    LayoutHeader()
    LayoutBody()
    RefreshInputs()
    if bodyScroll and bodyScroll.scrollFrame then
        bodyScroll.scrollFrame:SetVerticalScroll(0)
    end
    viewFrame:Show()
end

function EbonholdHub.SettingsView.Unmount()
    if not viewFrame then return end
    viewFrame:Hide()
end

function EbonholdHub.SettingsView.Init()
    if EbonholdHub.Build and EbonholdHub.Build.OnActiveChanged then
        EbonholdHub.Build.OnActiveChanged(RefreshHeader)
    end
end
