EbonholdHub.PublicBuildsView = {}

local SW = EbonholdHub.SiteWidgets
local ST = EbonholdHub.SiteTheme
local SC = ST.C
local SL = ST.Layout
local FLAT = ST.FLAT

local UI = EbonholdHub.UI
local C_LEGACY = UI.Colors

local PAGE_SIZE = 8
local ROW_H = 60
local ECHO_SIZE = 26
local ECHO_GAP = 6
local CHIP_H = 26
local CHIP_GAP = 6
local CHIP_PAD_X = 10

local CLASS_COLORS = ST.CLASS_COLORS
local CLASS_DISPLAY = ST.CLASS_DISPLAY
local CLASS_TOKENS = { "WARRIOR", "PALADIN", "HUNTER", "ROGUE", "PRIEST", "DEATHKNIGHT", "SHAMAN", "MAGE", "WARLOCK", "DRUID" }

local viewFrame
local rowPool = {}
local pageNumLabel, pageOfLabel, prevBtn, nextBtn
local scrollHolder, scrollFrame, scrollChild
local emptyCatalogPanel, emptyTitle, emptyHint, emptyWebsiteBtn, emptyImportBtn
local state = { builds = {}, allBuilds = {}, page = 1, totalPages = 1 }

local filterClass, filterSpec
local countLabel
local catalogPanel, filterHeader, classChipScroll, classChipRow
local specChipScroll, specChipRow, paginationBar
local classChips = {}
local specChips = {}

local FILTER_TOP_H = 28
local CHIP_ROW_Y = -32

local function EchoStripWidth(slotCount)
    if slotCount <= 0 then return 1 end
    return slotCount * ECHO_SIZE + (slotCount - 1) * ECHO_GAP
end

local function ShowImportDialog()
    if EbonholdHub.ExportImport and EbonholdHub.ExportImport.ShowImportDialog then
        EbonholdHub.ExportImport.ShowImportDialog()
    end
end

local function UpdateFilterScrollWidths()
    if not filterHeader then return end
    local w = filterHeader:GetWidth()
    if not w or w <= 0 then return end
    local chipW = math.max(1, w - 28)
    if classChipScroll then classChipScroll:SetWidth(chipW) end
    if specChipScroll then specChipScroll:SetWidth(chipW) end
end

local function OpenWebsite()
    if EbonholdHub.Catalog and EbonholdHub.Catalog.ShowWebsiteLink then
        EbonholdHub.Catalog.ShowWebsiteLink()
    elseif EbonholdHub.OpenExternalURL and EbonholdHub.Catalog then
        EbonholdHub.OpenExternalURL(EbonholdHub.Catalog.WEBSITE_URL)
    end
end

local function UpdateEmptyState(filteredOut)
    if not emptyCatalogPanel then return end
    if filteredOut then
        if emptyTitle then emptyTitle:SetText("No builds match the current filters.") end
        if emptyHint then
            emptyHint:SetText("Try another class or spec, or browse all community builds on the website.")
        end
        if emptyImportBtn then emptyImportBtn:Hide() end
    else
        if emptyTitle then emptyTitle:SetText("Your catalog is empty") end
        if emptyHint then
            emptyHint:SetText(
                "Browse community builds on the website, or import your own build from an export code."
            )
        end
        if emptyImportBtn then emptyImportBtn:Show() end
    end
end

local RefreshView, GetFilteredBuilds, RefreshFilterChips

local function FetchPublicBuilds()
    return EbonholdHub.Build.ListPublic()
end

local function ResolveClassFilter(classVal)
    if classVal == nil then return nil end
    if CLASS_DISPLAY[classVal] then return classVal end
    for token, name in pairs(CLASS_DISPLAY) do
        if name == classVal then return token end
    end
    return classVal
end

local function FindImportedCopy(publicBuildId)
    return EbonholdHub.Catalog.FindImportedCopy(publicBuildId)
end

local function ImportBuild(build)
    local imported = EbonholdHub.Catalog.ImportBuild(build)
    EbonholdHub.ViewRouter.Show("buildOverview", { build = imported })
end

local function UpdateLocalBuild(localBuild, publicBuild)
    local updated = EbonholdHub.Catalog.UpdateImported(localBuild, publicBuild)
    EbonholdHub.ViewRouter.Show("buildOverview", { build = updated })
end

local function SetOutlineText(btn, text)
    if not btn then return end
    if not btn._siteLabel then
        for _, region in ipairs({ btn:GetRegions() }) do
            if region.GetText and region.SetText then
                btn._siteLabel = region
                break
            end
        end
    end
    if btn._siteLabel then btn._siteLabel:SetText(text or "") end
end

local function CreateChipScroller(parent, height)
    local scroll = CreateFrame("ScrollFrame", nil, parent)
    scroll:SetHeight(height)
    scroll:EnableMouseWheel(true)
    local row = CreateFrame("Frame", nil, scroll)
    row:SetHeight(height)
    scroll:SetScrollChild(row)
    scroll:SetScript("OnMouseWheel", function(self, delta)
        local child = self:GetScrollChild()
        if not child then return end
        local max = math.max(0, child:GetWidth() - self:GetWidth())
        if max <= 0 then return end
        local pos = self:GetHorizontalScroll() - delta * 40
        self:SetHorizontalScroll(math.max(0, math.min(max, pos)))
    end)
    return scroll, row
end

local function StyleFilterChip(chip, active, classColor)
    if active and classColor then
        local border = ST.BlendRgb(classColor, SC.border, 0.55)
        local bg = ST.ClassTint(classColor, 0.14)
        chip._bg:SetVertexColor(bg[1], bg[2], bg[3], 1)
        SW.SetBorderColor(chip._border, border)
        chip._label:SetTextColor(classColor[1], classColor[2], classColor[3])
        if chip._icon then chip._icon:SetVertexColor(1, 1, 1) end
    elseif active then
        chip._bg:SetVertexColor(unpack(SC.accentBg12))
        SW.SetBorderColor(chip._border, SC.accentBorder35)
        chip._label:SetTextColor(unpack(SC.text))
        if chip._icon then chip._icon:SetVertexColor(1, 1, 1) end
    else
        chip._bg:SetVertexColor(unpack(SC.elementBg))
        SW.SetBorderColor(chip._border, SC.border)
        chip._label:SetTextColor(unpack(SC.textDim))
        if chip._icon then chip._icon:SetVertexColor(0.75, 0.75, 0.8) end
    end
end

local function CreateFilterChip(parent, label, classToken, onClick)
    local chip = CreateFrame("Button", nil, parent)
    chip:SetHeight(CHIP_H)
    chip._bg = SW.Fill(chip, SC.elementBg)
    chip._border = SW.ThinBorder(chip, SC.border, 1)

    local x = CHIP_PAD_X
    if classToken then
        local iconHolder = CreateFrame("Frame", nil, chip)
        iconHolder:SetSize(16, 16)
        iconHolder:SetPoint("LEFT", CHIP_PAD_X, 0)
        local icon = iconHolder:CreateTexture(nil, "ARTWORK")
        icon:SetAllPoints(iconHolder)
        SW.SetClassIcon(icon, classToken)
        chip._icon = icon
        x = CHIP_PAD_X + 16 + 5
    end

    chip._label = SW.Label(chip, label, 11, SC.textDim, false, "medium")
    chip._label:SetPoint("LEFT", x, 0)
    chip._label:SetPoint("RIGHT", -CHIP_PAD_X, 0)
    chip:SetWidth(chip._label:GetStringWidth() + x + CHIP_PAD_X)

    chip:SetScript("OnClick", onClick)
    chip:SetScript("OnEnter", function(self)
        if not self._active then
            SW.SetBorderColor(self._border, SC.borderHover)
            self._label:SetTextColor(unpack(SC.text))
        end
    end)
    chip:SetScript("OnLeave", function(self)
        StyleFilterChip(self, self._active, self._classColor)
    end)
    return chip
end

local function LayoutChipRow(row, chips)
    local x = 0
    for _, chip in ipairs(chips) do
        if chip:IsShown() then
            chip:ClearAllPoints()
            chip:SetPoint("LEFT", row, "LEFT", x, 0)
            x = x + chip:GetWidth() + CHIP_GAP
        end
    end
    row:SetWidth(math.max(1, x))
end

local function BuildFilterChips()
    if not classChipRow or #classChips > 0 then return end

    local allChip = CreateFilterChip(classChipRow, "All classes", nil, function()
        filterClass = nil
        filterSpec = nil
        RefreshFilterChips()
        RefreshView()
    end)
    classChips[#classChips + 1] = allChip

    for _, token in ipairs(CLASS_TOKENS) do
        local chip = CreateFilterChip(classChipRow, CLASS_DISPLAY[token] or token, token, function()
            if filterClass == token then
                filterClass = nil
                filterSpec = nil
            else
                filterClass = token
                filterSpec = nil
            end
            RefreshFilterChips()
            RefreshView()
        end)
        chip._classColor = CLASS_COLORS[token]
        classChips[#classChips + 1] = chip
    end
    LayoutChipRow(classChipRow, classChips)
end

local function EnsureSpecChips()
    if not specChipRow or specChips._built then return end
    specChips._built = true

    local allSpec = CreateFilterChip(specChipRow, "All specs", nil, function()
        filterSpec = nil
        RefreshFilterChips()
        RefreshView()
    end)
    specChips[#specChips + 1] = allSpec

    for _, token in ipairs(CLASS_TOKENS) do
        local specs = EbonholdHub.SpecData and EbonholdHub.SpecData[token] or {}
        for i, entry in ipairs(specs) do
            local chip = CreateFilterChip(specChipRow, entry.name, nil, function()
                if filterClass ~= token then return end
                if filterSpec == i then filterSpec = nil else filterSpec = i end
                RefreshFilterChips()
                RefreshView()
            end)
            chip._specClass = token
            chip._specIndex = i
            specChips[#specChips + 1] = chip
        end
    end
end

RefreshFilterChips = function()
    if not classChipRow then return end
    BuildFilterChips()
    EnsureSpecChips()

    classChips[1]._active = not filterClass
    StyleFilterChip(classChips[1], classChips[1]._active, nil)
    for i = 2, #classChips do
        local token = CLASS_TOKENS[i - 1]
        local cc = CLASS_COLORS[token]
        classChips[i]._active = (filterClass == token)
        StyleFilterChip(classChips[i], classChips[i]._active, classChips[i]._active and cc or nil)
    end

    if specChipScroll and filterHeader then
        if filterClass then
            specChipScroll:Show()
            filterHeader:SetHeight(88)
            local cc = CLASS_COLORS[filterClass]
            local visible = {}
            visible[#visible + 1] = specChips[1]
            specChips[1]._active = not filterSpec
            specChips[1]._classColor = cc
            StyleFilterChip(specChips[1], specChips[1]._active, specChips[1]._active and cc or nil)

            for i = 2, #specChips do
                local chip = specChips[i]
                local show = chip._specClass == filterClass
                if show then
                    chip:Show()
                    chip._active = (filterSpec == chip._specIndex)
                    chip._classColor = cc
                    StyleFilterChip(chip, chip._active, chip._active and cc or nil)
                    visible[#visible + 1] = chip
                else
                    chip:Hide()
                end
            end
            LayoutChipRow(specChipRow, visible)
        else
            specChipScroll:Hide()
            filterHeader:SetHeight(62)
        end
    end

    if countLabel then
        local n = #(state.builds or {})
        local text = string.format("%d build%s", n, n == 1 and "" or "s")
        if filterClass and filterSpec then
            text = text .. "  ·  " .. (CLASS_DISPLAY[filterClass] or "") .. " " .. ST.SpecDisplay(filterClass, filterSpec)
        end
        countLabel:SetText(text)
    end
end

local function CreateEchoSlot(parent)
    local slot = CreateFrame("Frame", nil, parent)
    slot:SetSize(ECHO_SIZE, ECHO_SIZE)
    slot._bg = SW.Fill(slot, SC.bg)
    slot._border = SW.ThinBorder(slot, SC.border, 1)
    slot._icon = slot:CreateTexture(nil, "ARTWORK")
    slot._icon:SetPoint("TOPLEFT", 2, -2)
    slot._icon:SetPoint("BOTTOMRIGHT", -2, 2)
    slot._icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
    slot._icon:Hide()
    return slot
end

local function ApplyRowGradient(row, classToken)
    local cc = CLASS_COLORS[classToken] or SC.textMuted
    if not row._grad then
        row._grad = row:CreateTexture(nil, "BACKGROUND")
        row._grad:SetAllPoints(row)
    end
    local left = ST.ClassTint(cc, 0.14)
    row._grad:SetTexture(FLAT)
    row._grad:SetGradientAlpha("HORIZONTAL",
        left[1], left[2], left[3], 1,
        SC.bg[1], SC.bg[2], SC.bg[3], 0)
    row._stripe:SetVertexColor(cc[1], cc[2], cc[3], 1)
end

local function EnsureEchoSlots(row)
    local lockedN = EbonholdHub.Build.GetLockedSlotCount()
    row._echoSlots = row._echoSlots or {}
    for i = #row._echoSlots + 1, lockedN do
        local slot = CreateEchoSlot(row._echoStrip)
        slot:SetPoint("LEFT", row._echoStrip, "LEFT", (i - 1) * (ECHO_SIZE + ECHO_GAP), 0)
        slot:SetScript("OnEnter", function(self)
            if not self._spellId then return end
            GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
            GameTooltip:ClearLines()
            local spellName = GetSpellInfo(self._spellId)
            if spellName then GameTooltip:AddLine(spellName, 1, 0.82, 0) end
            GameTooltip:Show()
        end)
        slot:SetScript("OnLeave", function() GameTooltip:Hide() end)
        row._echoSlots[i] = slot
    end
    row._echoStrip:SetSize(EchoStripWidth(lockedN), ECHO_SIZE)
    return lockedN
end

local function CreateBuildRow(parent)
    local row = CreateFrame("Frame", nil, parent)
    row:SetHeight(ROW_H)

    row._stripe = row:CreateTexture(nil, "ARTWORK")
    row._stripe:SetTexture(FLAT)
    row._stripe:SetWidth(3)
    row._stripe:SetPoint("TOPLEFT", row, "TOPLEFT", 0, 0)
    row._stripe:SetPoint("BOTTOMLEFT", row, "BOTTOMLEFT", 0, 0)

    row._sep = row:CreateTexture(nil, "BORDER")
    row._sep:SetTexture(FLAT)
    row._sep:SetVertexColor(unpack(SC.borderSoft))
    row._sep:SetHeight(1)
    row._sep:SetPoint("BOTTOMLEFT", row, "BOTTOMLEFT", 0, 0)
    row._sep:SetPoint("BOTTOMRIGHT", row, "BOTTOMRIGHT", 0, 0)

    row._iconHolder = SW.CreateClassIconHolder(row, "PRIEST", 1, 40)
    row._iconHolder:SetPoint("LEFT", row._stripe, "RIGHT", 12, 0)

    row._importBtn = SW.CreateOutlineButton(row, "Select", 76)
    row._importBtn:SetPoint("RIGHT", row, "RIGHT", -12, 0)
    row._importBtn:SetFrameLevel(row:GetFrameLevel() + 4)

    row._deleteBtn = SW.CreateOutlineButton(row, "Delete", 56)
    row._deleteBtn:SetPoint("RIGHT", row._importBtn, "LEFT", -6, 0)
    row._deleteBtn:SetFrameLevel(row:GetFrameLevel() + 4)
    SetOutlineText(row._deleteBtn, "Delete")
    if row._deleteBtn._siteLabel then
        row._deleteBtn._siteLabel:SetTextColor(unpack(C_LEGACY.danger or SC.text))
    end

    row._echoStrip = CreateFrame("Frame", nil, row)
    row._echoStrip:SetPoint("RIGHT", row._deleteBtn, "LEFT", -12, 0)
    row._echoSlots = {}
    EnsureEchoSlots(row)

    row._titleLabel = SW.Label(row, "", 13, SC.text, false, "semibold")
    row._titleLabel:SetPoint("TOPLEFT", row._iconHolder, "TOPRIGHT", 12, -2)
    row._titleLabel:SetPoint("RIGHT", row._echoStrip, "LEFT", -12, -2)
    row._titleLabel:SetJustifyH("LEFT")

    row._metaLabel = SW.Label(row, "", 10, SC.textMuted)
    row._metaLabel:SetPoint("TOPLEFT", row._titleLabel, "BOTTOMLEFT", 0, -2)
    row._metaLabel:SetPoint("RIGHT", row._echoStrip, "LEFT", -12, -2)
    row._metaLabel:SetJustifyH("LEFT")

    row:EnableMouse(true)
    row:SetScript("OnEnter", function(self)
        self._hoverBg = self._hoverBg or SW.Fill(self, SC.elementBg)
        self._hoverBg:SetAlpha(0.45)
    end)
    row:SetScript("OnLeave", function(self)
        if self._hoverBg then self._hoverBg:SetAlpha(0) end
    end)

    row:Hide()
    return row
end

local function UpdateRowIcon(row, build)
    local key = (build.class or "") .. ":" .. tostring(build.spec or 1)
    if row._iconKey ~= key then
        if row._iconHolder then row._iconHolder:Hide() end
        row._iconHolder = SW.CreateClassIconHolder(row, build.class, build.spec or 1, 40)
        row._iconHolder:SetPoint("LEFT", row._stripe, "RIGHT", 12, 0)
        row._titleLabel:ClearAllPoints()
        row._titleLabel:SetPoint("TOPLEFT", row._iconHolder, "TOPRIGHT", 12, -2)
        row._titleLabel:SetPoint("RIGHT", row._echoStrip, "LEFT", -12, -2)
        row._metaLabel:ClearAllPoints()
        row._metaLabel:SetPoint("TOPLEFT", row._titleLabel, "BOTTOMLEFT", 0, -2)
        row._metaLabel:SetPoint("RIGHT", row._echoStrip, "LEFT", -12, -2)
        row._iconKey = key
    end
end

local function PopulateRow(row, build)
    row._build = build
    local cc = CLASS_COLORS[build.class] or SC.textMuted
    ApplyRowGradient(row, build.class)
    UpdateRowIcon(row, build)

    row._titleLabel:SetText(build.title or "Untitled")
    row._titleLabel:SetTextColor(unpack(cc))

    local specName = ST.SpecDisplay(build.class, build.spec or 1)
    local author = build.author or "Unknown"
    local modified = build.lastModified or ""
    row._metaLabel:SetText(string.format("by %s  ·  %s  ·  %s", author, specName, modified))

    local lockeds = build.lockedEchoes
    local lockedN = EnsureEchoSlots(row)
    for i = 1, lockedN do
        local slot = row._echoSlots[i]
        local spellId = lockeds and lockeds[i]
        if spellId then
            slot._icon:SetTexture(select(3, GetSpellInfo(spellId)))
            slot._icon:Show()
            slot._spellId = spellId
        else
            slot._icon:Hide()
            slot._spellId = nil
        end
    end

    SetOutlineText(row._importBtn, "Select")
    row._importBtn:Enable()
    row._importBtn:SetScript("OnClick", function()
        local localCopy = FindImportedCopy(build.id)
        if localCopy and build.lastModified == localCopy._importedAt then
            EbonholdHub.ViewRouter.Show("buildOverview", { build = localCopy })
        elseif localCopy and build.lastModified ~= localCopy._importedAt then
            UpdateLocalBuild(localCopy, build)
        else
            ImportBuild(build)
        end
    end)

    row._deleteBtn:SetScript("OnClick", function()
        EbonholdHub.Catalog.ConfirmRemove(build, function() RefreshView() end)
    end)
end

local function RefreshPaginationControls()
    if not paginationBar then return end
    if state.totalPages > 1 then
        paginationBar:Show()
        if state.page > 1 then prevBtn:Enable() else prevBtn:Disable() end
        if state.page < state.totalPages then nextBtn:Enable() else nextBtn:Disable() end
        if pageNumLabel then pageNumLabel:SetText(tostring(state.page)) end
        if pageOfLabel then pageOfLabel:SetText("of " .. state.totalPages) end
    else
        paginationBar:Hide()
    end
end

local function Render()
    local all = state.builds or {}
    if #all == 0 then
        for _, row in ipairs(rowPool) do row:Hide() end
        scrollChild:SetHeight(1)
        scrollFrame:SetVerticalScroll(0)
        RefreshFilterChips()
        pageNumLabel:SetText("1")
        pageOfLabel:SetText("of 1")
        if prevBtn then prevBtn:Disable() end
        if nextBtn then nextBtn:Disable() end
        if paginationBar then paginationBar:Hide() end
        if scrollFrame then scrollFrame:Hide() end
        local filteredOut = #(state.allBuilds or {}) > 0
        UpdateEmptyState(filteredOut)
        if emptyCatalogPanel then emptyCatalogPanel:Show() end
        return
    end
    if scrollFrame then scrollFrame:Show() end
    if emptyCatalogPanel then emptyCatalogPanel:Hide() end
    RefreshFilterChips()

    local startIdx = (state.page - 1) * PAGE_SIZE + 1
    local endIdx = math.min(startIdx + PAGE_SIZE - 1, #all)
    local totalHeight = 0

    for i = startIdx, endIdx do
        local poolIdx = i - startIdx + 1
        if not rowPool[poolIdx] then
            rowPool[poolIdx] = CreateBuildRow(scrollChild)
        end
        local row = rowPool[poolIdx]
        PopulateRow(row, all[i])
        row:ClearAllPoints()
        row:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", 0, -totalHeight)
        row:SetPoint("RIGHT", scrollChild, "RIGHT", 0, 0)
        row:Show()
        totalHeight = totalHeight + ROW_H
    end
    for i = endIdx - startIdx + 2, #rowPool do
        rowPool[i]:Hide()
    end

    scrollChild:SetHeight(math.max(1, totalHeight))
    scrollFrame:SetVerticalScroll(0)
    RefreshPaginationControls()
end

GetFilteredBuilds = function()
    local activeClass = ResolveClassFilter(filterClass)
    local activeSpec = filterSpec
    local all = FetchPublicBuilds()
    local filtered = {}
    for _, build in ipairs(all) do
        if activeClass and build.class ~= activeClass then
        elseif activeSpec and build.spec ~= activeSpec then
        else
            filtered[#filtered + 1] = build
        end
    end
    return filtered
end

RefreshView = function()
    if not scrollFrame then return end
    local w = scrollFrame:GetWidth()
    if w and w > 0 and scrollChild then scrollChild:SetWidth(w) end
    state.allBuilds = FetchPublicBuilds()
    state.builds = GetFilteredBuilds()
    state.page = 1
    state.totalPages = math.max(1, math.ceil(#state.builds / PAGE_SIZE))
    scrollFrame:SetVerticalScroll(0)
    Render()
end

local function BuildViewFrame(parent)
    local f = CreateFrame("Frame", nil, parent)
    local body = CreateFrame("Frame", nil, f)
    body:SetPoint("TOPLEFT", SL.PAD, -SL.PAD)
    body:SetPoint("BOTTOMRIGHT", -SL.PAD, SL.PAD)

    catalogPanel = CreateFrame("Frame", nil, body)
    catalogPanel:SetPoint("TOPLEFT", body, "TOPLEFT", 0, 0)
    catalogPanel:SetPoint("BOTTOMRIGHT", body, "BOTTOMRIGHT", 0, 0)
    SW.Fill(catalogPanel, SC.bg)
    catalogPanel._border = SW.ThinBorder(catalogPanel, SC.border, 1)

    filterHeader = CreateFrame("Frame", nil, catalogPanel)
    filterHeader:SetPoint("TOPLEFT", catalogPanel, "TOPLEFT", 0, 0)
    filterHeader:SetPoint("TOPRIGHT", catalogPanel, "TOPRIGHT", 0, 0)
    filterHeader:SetHeight(62)
    SW.Fill(filterHeader, SC.bgElevated)
    local filterSep = filterHeader:CreateTexture(nil, "ARTWORK")
    filterSep:SetTexture(FLAT)
    filterSep:SetVertexColor(unpack(SC.border))
    filterSep:SetHeight(1)
    filterSep:SetPoint("BOTTOMLEFT", filterHeader, "BOTTOMLEFT", 0, 0)
    filterSep:SetPoint("BOTTOMRIGHT", filterHeader, "BOTTOMRIGHT", 0, 0)

    countLabel = SW.Label(filterHeader, "0 builds", 10, SC.textMuted)
    countLabel:SetPoint("TOPLEFT", 14, -8)

    local importHeaderBtn = SW.CreateAccentButton(filterHeader, "Import Build", ShowImportDialog)
    importHeaderBtn:SetSize(110, 26)
    importHeaderBtn:SetPoint("TOPRIGHT", -14, -6)

    classChipScroll, classChipRow = CreateChipScroller(filterHeader, CHIP_H)
    classChipScroll:SetPoint("TOPLEFT", 14, CHIP_ROW_Y)
    classChipScroll:SetPoint("TOPRIGHT", -14, CHIP_ROW_Y)

    specChipScroll, specChipRow = CreateChipScroller(filterHeader, CHIP_H)
    specChipScroll:SetPoint("TOPLEFT", 14, -64)
    specChipScroll:SetPoint("TOPRIGHT", -14, -64)
    specChipScroll:Hide()

    filterHeader:SetScript("OnSizeChanged", UpdateFilterScrollWidths)

    paginationBar = CreateFrame("Frame", nil, catalogPanel)
    paginationBar:SetPoint("BOTTOMLEFT", catalogPanel, "BOTTOMLEFT", 0, 0)
    paginationBar:SetPoint("BOTTOMRIGHT", catalogPanel, "BOTTOMRIGHT", 0, 0)
    paginationBar:SetHeight(40)
    SW.Fill(paginationBar, SC.elementBg)
    local pagSep = paginationBar:CreateTexture(nil, "ARTWORK")
    pagSep:SetTexture(FLAT)
    pagSep:SetVertexColor(unpack(SC.border))
    pagSep:SetHeight(1)
    pagSep:SetPoint("TOPLEFT", paginationBar, "TOPLEFT", 0, 0)
    pagSep:SetPoint("TOPRIGHT", paginationBar, "TOPRIGHT", 0, 0)

    prevBtn = SW.CreateOutlineButton(paginationBar, "Previous", 88)
    prevBtn:SetPoint("LEFT", 12, 0)
    prevBtn:SetScript("OnClick", function()
        if state.page > 1 then
            state.page = state.page - 1
            scrollFrame:SetVerticalScroll(0)
            Render()
        end
    end)

    nextBtn = SW.CreateOutlineButton(paginationBar, "Next", 88)
    nextBtn:SetPoint("RIGHT", -12, 0)
    nextBtn:SetScript("OnClick", function()
        if state.page < state.totalPages then
            state.page = state.page + 1
            scrollFrame:SetVerticalScroll(0)
            Render()
        end
    end)

    local pageWrap = CreateFrame("Frame", nil, paginationBar)
    pageWrap:SetPoint("CENTER")
    SW.Label(pageWrap, "Page", 10, SC.textMuted):SetPoint("LEFT", 0, 0)
    pageNumLabel = SW.Label(pageWrap, "1", 12, SC.text, false, "semibold")
    pageNumLabel:SetPoint("LEFT", 34, 0)
    pageOfLabel = SW.Label(pageWrap, "of 1", 10, SC.textMuted)
    pageOfLabel:SetPoint("LEFT", pageNumLabel, "RIGHT", 6, 0)
    pageWrap:SetWidth(120)

    scrollHolder, scrollChild = SW.CreatePlainScroll(catalogPanel)

    emptyCatalogPanel = CreateFrame("Frame", nil, catalogPanel)
    emptyCatalogPanel:SetSize(520, 140)
    emptyCatalogPanel:SetPoint("CENTER", scrollHolder, "CENTER", 0, 0)
    emptyCatalogPanel:SetFrameLevel(scrollHolder:GetFrameLevel() + 2)
    emptyCatalogPanel:Hide()

    emptyTitle = SW.Label(emptyCatalogPanel, "Your catalog is empty", 14, SC.textDim, false, "semibold")
    emptyTitle:SetPoint("TOP", emptyCatalogPanel, "TOP", 0, 0)
    emptyTitle:SetWidth(500)
    emptyTitle:SetJustifyH("CENTER")

    emptyHint = SW.Label(emptyCatalogPanel,
        "Browse community builds on the website, or import your own build from an export code.",
        11, SC.textMuted)
    emptyHint:SetPoint("TOP", emptyTitle, "BOTTOM", 0, -10)
    emptyHint:SetWidth(480)
    emptyHint:SetJustifyH("CENTER")
    emptyHint:SetWordWrap(true)

    local btnRow = CreateFrame("Frame", nil, emptyCatalogPanel)
    btnRow:SetSize(280, 30)
    btnRow:SetPoint("TOP", emptyHint, "BOTTOM", 0, -16)

    emptyWebsiteBtn = SW.CreateAccentButton(btnRow, "Open website", OpenWebsite)
    emptyWebsiteBtn:SetSize(120, 28)
    emptyWebsiteBtn:SetPoint("LEFT", btnRow, "LEFT", 0, 0)

    emptyImportBtn = SW.CreateOutlineButton(btnRow, "Import Build", 120)
    emptyImportBtn:SetPoint("LEFT", emptyWebsiteBtn, "RIGHT", 8, 0)
    emptyImportBtn:SetScript("OnClick", ShowImportDialog)

    scrollHolder:ClearAllPoints()
    scrollHolder:SetPoint("TOPLEFT", filterHeader, "BOTTOMLEFT", 0, 0)
    scrollHolder:SetPoint("BOTTOMRIGHT", paginationBar, "TOPRIGHT", 0, 0)
    scrollFrame = scrollHolder

    scrollFrame:SetScript("OnSizeChanged", function()
        local w = scrollFrame:GetWidth()
        if w and w > 0 then
            scrollChild:SetWidth(w)
            UpdateFilterScrollWidths()
            if state.builds and #state.builds > 0 then Render() end
        end
    end)

    filterClass = nil
    filterSpec = nil
    f._catalogLayoutV8 = true
    return f
end

local function EnsureBuilt(container)
    if viewFrame and not viewFrame._catalogLayoutV8 then
        viewFrame = nil
        wipe(rowPool)
        wipe(classChips)
        specChips._built = nil
        while #specChips > 0 do specChips[#specChips] = nil end
    end
    if viewFrame then return end
    viewFrame = BuildViewFrame(container)
    RefreshFilterChips()
end

function EbonholdHub.PublicBuildsView.Mount(container)
    EnsureBuilt(container)
    viewFrame:SetParent(container)
    viewFrame:ClearAllPoints()
    viewFrame:SetAllPoints(container)
    state.allBuilds = FetchPublicBuilds()
    state.builds = GetFilteredBuilds()
    state.page = 1
    state.totalPages = math.max(1, math.ceil(#state.builds / PAGE_SIZE))
    scrollFrame:SetVerticalScroll(0)
    Render()
    viewFrame:Show()
end

function EbonholdHub.PublicBuildsView.Unmount()
    if EbonholdHub.UI and EbonholdHub.UI.CloseAllDropdowns then
        EbonholdHub.UI.CloseAllDropdowns()
    end
    if viewFrame then viewFrame:Hide() end
end

function EbonholdHub.PublicBuildsView.RefreshIfMounted()
    if viewFrame and viewFrame:IsVisible() then RefreshView() end
end

function EbonholdHub.PublicBuildsView.Init()
end
