-- HousingDecorGuide/UI/Crates/HDG_CratePicker.lua
-- Modal decor picker for the Crates feature. Composition only — relies on
-- HDG_Carousel for category navigation, HDG_DecorFilter for predicates,
-- and HDG_Data for catalog access.
--
-- Public API:
--   HDG_CratePicker:Show(crateID)   -- opens modal targeting the given crate
--   HDG_CratePicker:Hide()
--
-- On Done, the selected decor IDs are committed via Crate_AddDecor.
-- ESC closes via UISpecialFrames.

local _, privateDB = ...
local Carousel = privateDB.HDG_Carousel
local Filter   = privateDB.HDG_DecorFilter

local M = {}

local TOP_CATEGORY_IDS = { 1, 2, 3, 4, 5, 6 }  -- Blizzard's 6 top-levels
local TOP_CARD_ATLAS = {
    [1] = "housing-icon-furnishings",
    [2] = "housing-icon-structural",
    [3] = "housing-icon-accents",
    [4] = "housing-icon-lighting",
    [5] = "housing-icon-functional",
    [6] = "housing-icon-nature",
}

local STATUS_CHIPS = {
    { key = "owned",     label = "Owned"     },
    { key = "missing",   label = "Missing"   },
    { key = "craftable", label = "Craftable" },
    { key = "vendor",    label = "Vendor"    },
}

local ROW_HEIGHT      = 28
local MODAL_WIDTH     = 920
local MODAL_HEIGHT    = 640
local CAROUSEL_HEIGHT = 70

-- ---------------------------------------------------------------------------
-- Item assembly: flatten HDG_Data catalog into a picker-friendly array
-- ---------------------------------------------------------------------------

local function _hasVendorSource(itemID)
    if not itemID then return false end
    local map = _G.HDG_VendorByItem
    return map and map[itemID] ~= nil
end

local _decorToRecipe = nil
local function _decorRecipeMap()
    if _decorToRecipe then return _decorToRecipe end
    _decorToRecipe = {}
    local recipes = _G.HDG_DecorRecipes or _G.HDG_RecipesDB
    if type(recipes) == "table" then
        for _, r in pairs(recipes) do
            local did = r and (r.decorID or r.recordID)
            if did then _decorToRecipe[did] = r end
        end
    end
    return _decorToRecipe
end

local function _buildItems()
    local items = {}
    if not (HDG_Data and HDG_Data.GetAllCatalog) then return items end
    local catalog = HDG_Data.GetAllCatalog() or {}
    local recipes = _decorRecipeMap()

    for decorID, row in pairs(catalog) do
        if row and row.name then
            local itemID    = row.itemID
            local collected = (HDG_Data.IsDecorCollected and HDG_Data.IsDecorCollected(decorID)) == true
                           or (itemID and HDG_Data.IsItemCollected and HDG_Data.IsItemCollected(itemID)) == true
            items[#items + 1] = {
                decorID         = decorID,
                itemID          = itemID,
                name            = row.name,
                _lowerName      = row.name:lower(),
                categoryID      = row.categoryID,
                subcategoryID   = row.subcategoryID,
                iconTexture     = row.iconTexture,
                iconAtlas       = row.iconAtlas,
                isCollected     = collected,
                isCraftable     = recipes[decorID] ~= nil,
                hasVendorSource = _hasVendorSource(itemID),
            }
        end
    end

    table.sort(items, function(a, b)
        return (a.name or "") < (b.name or "")
    end)
    return items
end

-- ---------------------------------------------------------------------------
-- Category info — cached on first build
-- ---------------------------------------------------------------------------

local function _catName(catID)
    if not catID then return "" end
    local info = C_HousingCatalog and C_HousingCatalog.GetCatalogCategoryInfo
        and C_HousingCatalog.GetCatalogCategoryInfo(catID)
    return (info and info.name) or ("Category " .. catID)
end

local function _subcatName(subID)
    if not subID then return "" end
    local info = C_HousingCatalog and C_HousingCatalog.GetCatalogSubcategoryInfo
        and C_HousingCatalog.GetCatalogSubcategoryInfo(subID)
    return (info and info.name) or ("Subcat " .. subID)
end

-- ---------------------------------------------------------------------------
-- Modal frame
-- ---------------------------------------------------------------------------

local function _createModal()
    local f = CreateFrame("Frame", "HDG_CratePickerModal", UIParent, "BackdropTemplate")
    f:SetSize(MODAL_WIDTH, MODAL_HEIGHT)
    f:SetPoint("CENTER")
    f:SetFrameStrata("DIALOG")
    f:SetMovable(true)
    f:EnableMouse(true)
    f:SetClampedToScreen(true)
    f:RegisterForDrag("LeftButton")
    f:SetScript("OnDragStart", f.StartMoving)
    f:SetScript("OnDragStop", f.StopMovingOrSizing)
    f:SetBackdrop({
        bgFile   = "Interface/Buttons/WHITE8X8",
        edgeFile = "Interface/Tooltips/UI-Tooltip-Border",
        edgeSize = 16, insets = { left = 4, right = 4, top = 4, bottom = 4 },
    })
    f:SetBackdropColor(0.04, 0.10, 0.16, 0.97)
    f:SetBackdropBorderColor(0.96, 0.80, 0.36, 1)
    f:Hide()

    -- ---------- Header strip ----------
    local title = f:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
    title:SetPoint("TOPLEFT", 16, -14)
    title:SetText("Add decor to crate")
    title:SetTextColor(0.96, 0.80, 0.36, 1)
    f.title = title

    local cratePill = f:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    cratePill:SetPoint("TOPLEFT", title, "BOTTOMLEFT", 0, -2)
    cratePill:SetTextColor(0.6, 0.78, 0.86, 1)
    f.cratePill = cratePill

    local closeBtn = CreateFrame("Button", nil, f, "UIPanelCloseButton")
    closeBtn:SetPoint("TOPRIGHT", -4, -4)
    closeBtn:SetScript("OnClick", function() M:Hide() end)
    f.closeBtn = closeBtn

    -- ---------- Top carousel (categories) ----------
    local topCarouselHost = CreateFrame("Frame", nil, f)
    topCarouselHost:SetPoint("TOPLEFT",  16, -52)
    topCarouselHost:SetPoint("TOPRIGHT", -16, -52)
    topCarouselHost:SetHeight(CAROUSEL_HEIGHT)
    f.topCarouselHost = topCarouselHost

    local top = Carousel:Create(topCarouselHost, {
        cardWidth = 130, cardHeight = 60,
        accent = { r = 0.96, g = 0.80, b = 0.36 },  -- gold
    })
    top:GetFrame():SetAllPoints(topCarouselHost)
    f.topCarousel = top

    -- ---------- Sub carousel (subcategories) ----------
    local subCarouselHost = CreateFrame("Frame", nil, f)
    subCarouselHost:SetPoint("TOPLEFT",  topCarouselHost, "BOTTOMLEFT",  0, -8)
    subCarouselHost:SetPoint("TOPRIGHT", topCarouselHost, "BOTTOMRIGHT", 0, -8)
    subCarouselHost:SetHeight(CAROUSEL_HEIGHT)
    f.subCarouselHost = subCarouselHost

    local sub = Carousel:Create(subCarouselHost, {
        cardWidth = 150, cardHeight = 60,
        accent = { r = 0.40, g = 0.85, b = 1.0 },  -- cyan
    })
    sub:GetFrame():SetAllPoints(subCarouselHost)
    f.subCarousel = sub

    -- ---------- Toolbar (search + status chips) ----------
    local toolbar = CreateFrame("Frame", nil, f)
    toolbar:SetPoint("TOPLEFT",  subCarouselHost, "BOTTOMLEFT",  0, -8)
    toolbar:SetPoint("TOPRIGHT", subCarouselHost, "BOTTOMRIGHT", 0, -8)
    toolbar:SetHeight(28)
    f.toolbar = toolbar

    local searchBox = CreateFrame("EditBox", nil, toolbar, "SearchBoxTemplate")
    searchBox:SetSize(200, 22)
    searchBox:SetPoint("LEFT", toolbar, "LEFT", 4, 0)
    searchBox:SetAutoFocus(false)
    f.searchBox = searchBox

    f.statusChips = {}
    local lastChip
    for i, def in ipairs(STATUS_CHIPS) do
        local chip = HDG_UIHelpers.CreateChipButton(toolbar, def.label, 80, false, nil)
        if i == 1 then
            chip:SetPoint("LEFT", searchBox, "RIGHT", 12, 0)
        else
            chip:SetPoint("LEFT", lastChip, "RIGHT", 4, 0)
        end
        chip._statusKey = def.key
        chip:SetScript("OnClick", function(c)
            c.isActive = not c.isActive
            HDG_UIHelpers.UpdateChipButtonState(c, c.isActive)
            f.statusSet = f.statusSet or {}
            f.statusSet[c._statusKey] = c.isActive or nil
            M:_RefreshList()
        end)
        f.statusChips[def.key] = chip
        lastChip = chip
    end

    -- ---------- Body: list + preview ----------
    local body = CreateFrame("Frame", nil, f)
    body:SetPoint("TOPLEFT",     toolbar,  "BOTTOMLEFT",  0, -10)
    body:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -16, 50)
    body:SetPoint("LEFT",        f, "LEFT",          16, 0)
    f.body = body

    -- LEFT: scroll list
    local listPanel = CreateFrame("Frame", nil, body, "BackdropTemplate")
    listPanel:SetPoint("TOPLEFT",     body, "TOPLEFT",     0, 0)
    listPanel:SetPoint("BOTTOMLEFT",  body, "BOTTOMLEFT",  0, 0)
    listPanel:SetWidth(540)
    listPanel:SetBackdrop({
        bgFile   = "Interface/Buttons/WHITE8X8",
        edgeFile = "Interface/Tooltips/UI-Tooltip-Border",
        edgeSize = 12, insets = { left = 2, right = 2, top = 2, bottom = 2 },
    })
    listPanel:SetBackdropColor(0.06, 0.13, 0.21, 0.6)
    listPanel:SetBackdropBorderColor(0.2, 0.3, 0.4, 1)
    f.listPanel = listPanel

    local scrollBox = CreateFrame("Frame", nil, listPanel, "WowScrollBoxList")
    scrollBox:SetPoint("TOPLEFT",     6, -6)
    scrollBox:SetPoint("BOTTOMRIGHT", -22, 6)
    f.scrollBox = scrollBox

    local scrollBar = CreateFrame("EventFrame", nil, listPanel, "MinimalScrollBar")
    scrollBar:SetPoint("TOPLEFT",    scrollBox, "TOPRIGHT", 2, 0)
    scrollBar:SetPoint("BOTTOMLEFT", scrollBox, "BOTTOMRIGHT", 2, 0)
    f.scrollBar = scrollBar

    local view = CreateScrollBoxListLinearView(0, 0, 0, 0, 0)
    view:SetElementExtent(ROW_HEIGHT)
    view:SetElementInitializer("Button", function(frame, data)
        if not frame._init then
            frame._init = true
            frame:SetHeight(ROW_HEIGHT)
            frame:RegisterForClicks("LeftButtonUp", "RightButtonUp")

            frame.bg = frame:CreateTexture(nil, "BACKGROUND")
            frame.bg:SetAllPoints()
            frame.bg:SetColorTexture(0, 0, 0, 0.0)

            frame.icon = frame:CreateTexture(nil, "ARTWORK")
            frame.icon:SetSize(20, 20)
            frame.icon:SetPoint("LEFT", 26, 0)

            frame.label = frame:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
            frame.label:SetPoint("LEFT", frame.icon, "RIGHT", 6, 0)
            frame.label:SetPoint("RIGHT", frame, "RIGHT", -120, 0)
            frame.label:SetJustifyH("LEFT")
            frame.label:SetWordWrap(false)

            frame.status = frame:CreateFontString(nil, "OVERLAY", "GameFontDisableSmall")
            frame.status:SetPoint("RIGHT", -8, 0)
            frame.status:SetJustifyH("RIGHT")

            frame.check = CreateFrame("CheckButton", nil, frame, "UICheckButtonTemplate")
            frame.check:SetSize(20, 20)
            frame.check:SetPoint("LEFT", 4, 0)
            frame.check:SetScript("OnClick", function(c)
                if not c._data or c._data.kind == "list" then return end
                M.selected[c._data.decorID] = c:GetChecked() and c._data or nil
                M:_RefreshFooter()
            end)

            -- List-mode action buttons (hidden in decor view)
            frame.addAll = CreateFrame("Button", nil, frame, "UIPanelButtonTemplate")
            frame.addAll:SetSize(60, 18)
            frame.addAll:SetPoint("RIGHT", frame, "RIGHT", -4, 0)

            frame.addMissing = CreateFrame("Button", nil, frame, "UIPanelButtonTemplate")
            frame.addMissing:SetSize(80, 18)
            frame.addMissing:SetPoint("RIGHT", frame.addAll, "LEFT", -4, 0)

            frame.hover = frame:CreateTexture(nil, "HIGHLIGHT")
            frame.hover:SetAllPoints()
            frame.hover:SetColorTexture(0.96, 0.80, 0.36, 0.10)

            frame:SetScript("OnClick", function(self, button)
                if button ~= "LeftButton" then return end
                if not self._data or self._data.kind == "list" then return end
                local newState = not (M.selected[self._data.decorID] ~= nil)
                M.selected[self._data.decorID] = newState and self._data or nil
                if self.check then self.check:SetChecked(newState) end
                M:_PreviewItem(self._data)
                M:_RefreshFooter()
            end)

            frame:SetScript("OnEnter", function(self)
                if self._data and self._data.itemID and self._data.kind ~= "list" then
                    GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
                    GameTooltip:SetItemByID(self._data.itemID)
                    GameTooltip:Show()
                end
            end)
            frame:SetScript("OnLeave", function() GameTooltip:Hide() end)
        end

        frame._data = data

        if data.kind == "list" then
            -- Style/Library row: name + count + Add All / Add Missing buttons.
            frame.icon:SetAtlas("Quest-Alliance-WaxSeal", false)
            frame.icon:SetTexture(nil)
            frame.icon:SetAtlas("Quest-Alliance-WaxSeal", false)
            frame.label:SetText(string.format("%s |cff8c8c8c· %d/%d|r",
                data.name or "?", data.ownedN or 0, data.totalN or 0))
            frame.status:Hide()
            frame.check:Hide()

            -- Normalize a list member's ID to a decorID for crate storage.
            -- Style members are itemIDs; Library members are already decorIDs.
            local function _toDecorID(id)
                if data.listType == "library" then return id end
                local entry = _G.HDG_AllDecorDB and _G.HDG_AllDecorDB[id]
                return (entry and entry[1]) or id
            end
            local function _isOwned(id)
                if data.listType == "library" then
                    return HDG_Data and HDG_Data.IsDecorCollected and HDG_Data.IsDecorCollected(id)
                end
                return HDG_Data and HDG_Data.IsItemCollected and HDG_Data.IsItemCollected(id)
            end

            local missing = (data.totalN or 0) - (data.ownedN or 0)
            frame.addAll:SetText(string.format("All %d", data.totalN or 0))
            frame.addAll:SetEnabled((data.totalN or 0) > 0)
            frame.addAll:SetScript("OnClick", function()
                for _, id in ipairs(data.memberIDs or {}) do
                    local did = _toDecorID(id)
                    if did then M.selected[did] = { decorID = did, kind = "decor" } end
                end
                M:_RefreshFooter()
            end)
            frame.addAll:Show()

            frame.addMissing:SetText(string.format("Missing %d", missing))
            frame.addMissing:SetEnabled(missing > 0)
            frame.addMissing:SetScript("OnClick", function()
                for _, id in ipairs(data.memberIDs or {}) do
                    if not _isOwned(id) then
                        local did = _toDecorID(id)
                        if did then M.selected[did] = { decorID = did, kind = "decor" } end
                    end
                end
                M:_RefreshFooter()
            end)
            frame.addMissing:Show()
        else
            -- Decor row (default).
            if data.iconAtlas then
                frame.icon:SetAtlas(data.iconAtlas, false)
            elseif data.iconTexture then
                frame.icon:SetTexture(data.iconTexture)
            else
                frame.icon:SetTexture(134400) -- ? icon
            end
            frame.label:SetText(data.name or ("Decor #" .. tostring(data.decorID)))

            local statusBits = {}
            if data.isCollected     then statusBits[#statusBits + 1] = "|cff8ee0a3owned|r"   end
            if data.isCraftable     then statusBits[#statusBits + 1] = "|cffe7c97ccraft|r"  end
            if data.hasVendorSource then statusBits[#statusBits + 1] = "|cff7cc7e7vendor|r"  end
            frame.status:SetText(table.concat(statusBits, "  "))
            frame.status:Show()

            frame.check:SetChecked(M.selected[data.decorID] ~= nil)
            frame.check:Show()
            frame.addAll:Hide()
            frame.addMissing:Hide()
        end
    end)
    ScrollUtil.InitScrollBoxListWithScrollBar(scrollBox, scrollBar, view)
    f.view = view

    -- RIGHT: 3D preview pane
    local previewPanel = CreateFrame("Frame", nil, body, "BackdropTemplate")
    previewPanel:SetPoint("TOPLEFT",     listPanel, "TOPRIGHT",     12, 0)
    previewPanel:SetPoint("BOTTOMRIGHT", body,      "BOTTOMRIGHT",   0, 0)
    previewPanel:SetBackdrop({
        bgFile   = "Interface/Buttons/WHITE8X8",
        edgeFile = "Interface/Tooltips/UI-Tooltip-Border",
        edgeSize = 12, insets = { left = 2, right = 2, top = 2, bottom = 2 },
    })
    previewPanel:SetBackdropColor(0.06, 0.13, 0.21, 0.6)
    previewPanel:SetBackdropBorderColor(0.2, 0.3, 0.4, 1)
    f.previewPanel = previewPanel

    local previewModel = CreateFrame("PlayerModel", nil, previewPanel)
    previewModel:SetPoint("TOPLEFT",     8, -8)
    previewModel:SetPoint("BOTTOMRIGHT", -8, 56)
    f.previewModel = previewModel

    local previewName = previewPanel:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    previewName:SetPoint("BOTTOMLEFT",  previewPanel, "BOTTOMLEFT",  10, 32)
    previewName:SetPoint("BOTTOMRIGHT", previewPanel, "BOTTOMRIGHT", -10, 32)
    previewName:SetJustifyH("LEFT")
    previewName:SetWordWrap(false)
    f.previewName = previewName

    local previewSubtitle = previewPanel:CreateFontString(nil, "OVERLAY", "GameFontDisableSmall")
    previewSubtitle:SetPoint("BOTTOMLEFT",  previewPanel, "BOTTOMLEFT",  10, 14)
    previewSubtitle:SetPoint("BOTTOMRIGHT", previewPanel, "BOTTOMRIGHT", -10, 14)
    previewSubtitle:SetJustifyH("LEFT")
    f.previewSubtitle = previewSubtitle

    -- ---------- Footer ----------
    local footer = CreateFrame("Frame", nil, f)
    footer:SetPoint("BOTTOMLEFT",  f, "BOTTOMLEFT",   16, 12)
    footer:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -16, 12)
    footer:SetHeight(30)
    f.footer = footer

    local summary = footer:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    summary:SetPoint("LEFT", 0, 0)
    summary:SetText("0 selected")
    f.summary = summary

    local doneBtn = CreateFrame("Button", nil, footer, "UIPanelButtonTemplate")
    doneBtn:SetSize(110, 24)
    doneBtn:SetPoint("RIGHT", 0, 0)
    doneBtn:SetText("Done")
    doneBtn:SetScript("OnClick", function() M:_Commit() end)
    f.doneBtn = doneBtn

    local cancelBtn = CreateFrame("Button", nil, footer, "UIPanelButtonTemplate")
    cancelBtn:SetSize(90, 24)
    cancelBtn:SetPoint("RIGHT", doneBtn, "LEFT", -6, 0)
    cancelBtn:SetText("Cancel")
    cancelBtn:SetScript("OnClick", function() M:Hide() end)
    f.cancelBtn = cancelBtn

    local bulkBtn = CreateFrame("Button", nil, footer, "UIPanelButtonTemplate")
    bulkBtn:SetSize(150, 24)
    bulkBtn:SetPoint("RIGHT", cancelBtn, "LEFT", -10, 0)
    bulkBtn:SetText("Add visible (0)")
    bulkBtn:SetScript("OnClick", function() M:_BulkAddVisible() end)
    f.bulkBtn = bulkBtn

    -- Search wires
    searchBox:HookScript("OnTextChanged", function(self)
        f.searchQuery = self:GetText() or ""
        M:_RefreshList()
    end)

    return f
end

-- ---------------------------------------------------------------------------
-- M state + handlers
-- ---------------------------------------------------------------------------

M.frame    = nil
M.crateID  = nil
M.selected = {}    -- decorID → row data
M.items    = {}    -- all item rows
M.filtered = {}    -- after carousel + search + status
M.activeCategory    = nil  -- categoryID; nil = "All"
M.activeSubcategory = nil  -- subcategoryID; nil = "All in category"

local function _ensureFrame()
    if M.frame then return M.frame end
    M.frame = _createModal()
    tinsert(_G.UISpecialFrames, "HDG_CratePickerModal")
    M.frame:SetScript("OnHide", function()
        M.crateID = nil
        M.selected = {}
        M.items    = {}
        M.filtered = {}
        M.activeCategory    = nil
        M.activeSubcategory = nil
    end)
    return M.frame
end

function M:_RebuildTopCarousel()
    local items = self.items
    local groups = Filter.GroupByCategory(items)
    local PURPLE = { r = 0.78, g = 0.50, b = 0.95 }

    local cards = {}

    -- "All" card
    local allCount = #items
    local allOwned = 0
    for i = 1, allCount do
        if items[i].isCollected then allOwned = allOwned + 1 end
    end
    cards[#cards + 1] = {
        id = "all",
        label = "All",
        sublabel = string.format("%d/%d", allOwned, allCount),
        progress = (allCount > 0) and (allOwned / allCount) or nil,
        atlas = "newplayertutorial-icon-mouse-leftbutton",
    }

    for _, cid in ipairs(TOP_CATEGORY_IDS) do
        local b = groups[cid]
        if b then
            cards[#cards + 1] = {
                id = "cat:" .. cid,
                label = _catName(cid),
                sublabel = string.format("%d/%d", b.owned, b.count),
                progress = (b.count > 0) and (b.owned / b.count) or nil,
                atlas = TOP_CARD_ATLAS[cid],
            }
        end
    end

    -- Non-Blizzard sources (purple-tinted accent) — Styles and Templates.
    -- Collections (HDG_CollectionDefinitions) follows once we audit that
    -- table's shape; Styles + Templates already share storage via
    -- Collection_ListByType.
    local store = _G.HousingDecorGuide and _G.HousingDecorGuide.Store
    if store and store.Collection_ListByType then
        local styleCount = 0
        for _ in pairs(store:Collection_ListByType("style")) do styleCount = styleCount + 1 end
        cards[#cards + 1] = {
            id = "styles",
            label = "My Styles",
            sublabel = string.format("%d", styleCount),
            atlas = "Quest-Alliance-WaxSeal",
            accent = PURPLE,
        }
        local libCount = 0
        for _ in pairs(store:Collection_ListByType("library")) do libCount = libCount + 1 end
        cards[#cards + 1] = {
            id = "templates",
            label = "Templates",
            sublabel = string.format("%d", libCount),
            atlas = "ChromieTime-32x32",
            accent = PURPLE,
        }
    end

    self.frame.topCarousel:SetItems(cards)
    local active = "all"
    if self.activeView == "styles"    then active = "styles"
    elseif self.activeView == "templates" then active = "templates"
    elseif self.activeCategory then active = "cat:" .. self.activeCategory end
    self.frame.topCarousel:SetActive(active)

    self.frame.topCarousel:SetOnSelect(function(id)
        if id == "styles" or id == "templates" then
            self.activeView        = id
            self.activeCategory    = nil
            self.activeSubcategory = nil
        elseif id == "all" then
            self.activeView        = "decor"
            self.activeCategory    = nil
            self.activeSubcategory = nil
        else
            local cid = tonumber(id:match("^cat:(%d+)$"))
            self.activeView        = "decor"
            self.activeCategory    = cid
            self.activeSubcategory = nil
        end
        self:_RebuildSubCarousel()
        self:_RefreshList()
    end)
end

function M:_RebuildSubCarousel()
    local cards = {}
    -- Sub-carousel is decor-view-only — Styles/Templates lists don't slice
    -- by Blizzard subcategory.
    if self.activeView == "decor" or self.activeView == nil then
        if self.activeCategory then
            local groups = Filter.GroupBySubcategory(self.items, self.activeCategory)
            local sids = {}
            for sid in pairs(groups) do sids[#sids + 1] = sid end
            table.sort(sids)

            cards[#cards + 1] = {
                id = "sub:all",
                label = "All in category",
                sublabel = nil,
                atlas = "Garr_BuildingShipment-Capstone-Icon",
            }
            for _, sid in ipairs(sids) do
                local b = groups[sid]
                cards[#cards + 1] = {
                    id = "sub:" .. sid,
                    label = _subcatName(sid),
                    sublabel = string.format("%d/%d", b.owned, b.count),
                    progress = (b.count > 0) and (b.owned / b.count) or nil,
                }
            end
        end
    end

    self.frame.subCarousel:SetItems(cards)
    self.frame.subCarousel:SetActive(self.activeSubcategory and ("sub:" .. self.activeSubcategory) or "sub:all")
    self.frame.subCarousel:SetOnSelect(function(id)
        if id == "sub:all" then
            self.activeSubcategory = nil
        else
            self.activeSubcategory = tonumber(id:match("^sub:(%d+)$"))
        end
        self:_RefreshList()
    end)
end

-- Style records store members in tier-specific fields, NOT under .members.
-- Resolve to a flat array of IDs regardless of tier.
local function _styleMemberIDs(coll)
    local out = {}
    if coll.tier == "curated" and type(coll.curatedItems) == "table" then
        for id in pairs(coll.curatedItems) do out[#out + 1] = id end
    elseif coll.tier == "snapshot" and coll.snapshot and type(coll.snapshot.items) == "table" then
        for id in pairs(coll.snapshot.items) do out[#out + 1] = id end
    elseif coll.tier == "custom" and type(coll.lockedItems) == "table" then
        for id in pairs(coll.lockedItems) do out[#out + 1] = id end
    end
    -- Fallback: also accept .members (Library entries follow the unified shape).
    if #out == 0 and type(coll.members) == "table" then
        for id in pairs(coll.members) do out[#out + 1] = id end
    end
    return out
end

-- Build picker rows for a "list view" (Styles or Templates). Each row is a
-- collection-shaped record with kind="list", showing name + member count +
-- owned/missing chip + Add-All / Add-Missing buttons.
local function _listViewItems(collType, queryText)
    local store = _G.HousingDecorGuide and _G.HousingDecorGuide.Store
    if not (store and store.Collection_ListByType) then return {} end
    local items = {}
    for collID, coll in pairs(store:Collection_ListByType(collType)) do
        local memberIDs
        if collType == "library" then
            memberIDs = {}
            for id in pairs(coll.members or {}) do memberIDs[#memberIDs + 1] = id end
        else
            -- Styles store members in curatedItems / lockedItems / snapshot.items.
            -- The IDs are itemIDs (not decorIDs) — owned check uses IsItemCollected.
            memberIDs = _styleMemberIDs(coll)
        end

        local owned, total = 0, #memberIDs
        for i = 1, total do
            local id = memberIDs[i]
            local isOwned
            if collType == "library" then
                isOwned = HDG_Data and HDG_Data.IsDecorCollected and HDG_Data.IsDecorCollected(id)
            else
                isOwned = HDG_Data and HDG_Data.IsItemCollected and HDG_Data.IsItemCollected(id)
            end
            if isOwned then owned = owned + 1 end
        end

        local displayName = coll.displayName or coll.name or "(unnamed)"
        local lower = displayName:lower()
        if not queryText or queryText == "" or lower:find(queryText:lower(), 1, true) then
            items[#items + 1] = {
                kind      = "list",
                listType  = collType,        -- "style" or "library"
                collID    = collID,
                name      = displayName,
                memberIDs = memberIDs,
                ownedN    = owned,
                totalN    = total,
                source    = coll.source,
            }
        end
    end
    table.sort(items, function(a, b)
        return (a.name or "") < (b.name or "")
    end)
    return items
end

function M:_RefreshList()
    if not self.frame then return end

    local items
    if self.activeView == "styles" then
        items = _listViewItems("style", self.frame.searchQuery)
    elseif self.activeView == "templates" then
        items = _listViewItems("library", self.frame.searchQuery)
    else
        items = Filter.Apply(
            self.items,
            self.frame.searchQuery,
            self.frame.statusSet,
            self.activeCategory,
            self.activeSubcategory)
    end
    self.filtered = items

    -- Build a fresh provider, populate it offline, then swap. Calling
    -- Insert on an already-attached provider fires a re-layout per call.
    local provider = CreateDataProvider()
    for i = 1, #items do
        provider:Insert(items[i])
    end
    self.frame.scrollBox:SetDataProvider(provider, ScrollBoxConstants.DiscardScrollPosition)
    self:_RefreshFooter()
end

function M:_RefreshFooter()
    if not self.frame then return end
    local n = 0
    for _ in pairs(self.selected) do n = n + 1 end
    self.frame.summary:SetText(string.format("%d selected", n))
    self.frame.bulkBtn:SetText(string.format("Add visible (%d)", #(self.filtered or {})))
end

function M:_PreviewItem(data)
    if not self.frame or not data then return end
    self.frame.previewName:SetText(data.name or "")
    self.frame.previewSubtitle:SetText(string.format(
        "%s · %s",
        _catName(data.categoryID) or "",
        _subcatName(data.subcategoryID) or ""))
    if data.itemID then
        local ok = pcall(self.frame.previewModel.SetItem, self.frame.previewModel, data.itemID)
        if not ok then self.frame.previewModel:ClearModel() end
    else
        self.frame.previewModel:ClearModel()
    end
end

function M:_BulkAddVisible()
    if not self.filtered then return end
    for _, data in ipairs(self.filtered) do
        self.selected[data.decorID] = data
    end
    -- Refresh row checkboxes (cheapest: re-run RefreshList; checkboxes re-bind)
    self:_RefreshList()
end

function M:_Commit()
    if not self.crateID then self:Hide(); return end
    local store = _G.HousingDecorGuide and _G.HousingDecorGuide.Store
    if not (store and store.Crate_AddDecor) then self:Hide(); return end
    local n = 0
    for did in pairs(self.selected) do
        if did then
            store:Crate_AddDecor(self.crateID, did)
            n = n + 1
        end
    end
    if DEFAULT_CHAT_FRAME and n > 0 then
        DEFAULT_CHAT_FRAME:AddMessage(string.format(
            "|cFFfcd34d[HDG]|r Added |cFF14b8a6%d|r decor to crate", n))
    end
    self:Hide()
end

function M:Show(crateID)
    local f = _ensureFrame()
    self.crateID  = crateID
    self.selected = {}
    self.items    = _buildItems()

    local store = _G.HousingDecorGuide and _G.HousingDecorGuide.Store
    local coll  = store and store.Collection_Get and store:Collection_Get(crateID)
    f.cratePill:SetText(string.format("|A:common-icon-forwardarrow:12:12|a Crate: %s",
        (coll and coll.name) or crateID or "?"))

    -- Reset chips + search + carousels
    f.searchBox:SetText("")
    f.searchQuery = ""
    f.statusSet = {}
    for _, chip in pairs(f.statusChips) do
        chip.isActive = false
        HDG_UIHelpers.UpdateChipButtonState(chip, false)
    end

    self.activeCategory    = nil
    self.activeSubcategory = nil

    self:_RebuildTopCarousel()
    self:_RebuildSubCarousel()
    self:_RefreshList()

    f.previewModel:ClearModel()
    f.previewName:SetText("")
    f.previewSubtitle:SetText("")

    f:Show()
end

function M:Hide()
    if self.frame then self.frame:Hide() end
end

if privateDB then privateDB.HDG_CratePicker = M end
return M
