-- ============================================================================
-- Vamoose's Companion - Settings: Companion Tab
-- Model preview, companion info, model variant grid
-- ============================================================================

VC = VC or {}
-- Methods are added to VC.Settings (defined in VC_Settings.lua)

local WHITE = "Interface\\Buttons\\WHITE8x8"

-- ============================================================================
-- COMPANION PAGE (two-column: preview left, info right; grid below)
-- ============================================================================

function VC.Settings:BuildCompanionPage(parent, W, pageH)
    local c = VC.Theme:GetScheme()
    local TB = VC.Constants.SETTINGS.TAB_BAR_H

    local page = CreateFrame("Frame", nil, parent)
    page:SetPoint("TOPLEFT", 0, -TB)
    page:SetPoint("TOPRIGHT", 0, -TB)
    page:SetHeight(pageH)
    page:Hide()
    self.pages["companion"] = page

    local INSET = 16
    local LEFT_COL_W = 220
    local GAP = 16
    local RIGHT_COL_X = INSET + LEFT_COL_W + GAP

    -- =========================================================================
    -- Left column: 3D model preview (220x260)
    -- =========================================================================
    local preview = CreateFrame("Frame", nil, page, "BackdropTemplate")
    preview:SetSize(LEFT_COL_W, 260)
    preview:SetPoint("TOPLEFT", INSET, -INSET)
    preview:SetBackdrop(VC.Theme.BACKDROP_PANEL)
    preview:SetBackdropColor(0, 0, 0, 0)  -- transparent so Forge-Background atlas shows through
    preview:SetBackdropBorderColor(c.border.r, c.border.g, c.border.b, 1)
    self.previewFrame = preview

    -- Preview background atlas
    local previewBg = preview:CreateTexture(nil, "BACKGROUND")
    previewBg:SetAllPoints()
    previewBg:SetAtlas("Forge-Background")

    -- ModelScene inside preview
    local previewScene = VC.UI:CreateModelScene(preview)
    previewScene:SetPoint("TOPLEFT", 5, -5)
    previewScene:SetSize(LEFT_COL_W - 10, 250)
    previewScene:SetFrameLevel(preview:GetFrameLevel() + 1)
    self.previewScene = previewScene

    -- Companion name (below preview)
    local nameLabel = page:CreateFontString(nil, "OVERLAY")
    nameLabel:SetPoint("TOPLEFT", INSET, -(INSET + 260 + 8))
    nameLabel:SetWidth(LEFT_COL_W)
    nameLabel:SetJustifyH("CENTER")
    nameLabel:SetFont("Fonts\\MORPHEUS.ttf", 13, "")
    nameLabel:SetTextColor(c.text_header.r, c.text_header.g, c.text_header.b, 1)
    nameLabel:SetShadowOffset(1, -1); nameLabel:SetShadowColor(0, 0, 0, 1)
    nameLabel.isHeader = true; VC.Theme:Register(nameLabel, "Text")
    self.companionNameLabel = nameLabel

    -- Title label (below name)
    local titleLabel = page:CreateFontString(nil, "OVERLAY")
    titleLabel:SetPoint("TOPLEFT", nameLabel, "BOTTOMLEFT", 0, -3)
    titleLabel:SetWidth(LEFT_COL_W)
    titleLabel:SetJustifyH("CENTER")
    titleLabel:SetFont("Fonts\\FRIZQT__.TTF", 12, "")
    titleLabel:SetTextColor(c.text_dim.r, c.text_dim.g, c.text_dim.b, 1)
    VC.Theme:Register(titleLabel, "DimText")
    self.companionTitleLabel = titleLabel

    -- =========================================================================
    -- Right column: Companion info (bond, mood, description)
    -- =========================================================================
    local RIGHT_COL_W = W - RIGHT_COL_X - INSET

    -- Description (1-2 lines)
    local descLabel = page:CreateFontString(nil, "OVERLAY")
    descLabel:SetPoint("TOPLEFT", RIGHT_COL_X, -INSET)
    descLabel:SetWidth(RIGHT_COL_W)
    descLabel:SetJustifyH("LEFT")
    descLabel:SetFont("Fonts\\FRIZQT__.TTF", 12, "")
    descLabel:SetTextColor(c.text_dim.r, c.text_dim.g, c.text_dim.b, 1)
    descLabel:SetWordWrap(true)
    descLabel:SetMaxLines(3)
    VC.Theme:Register(descLabel, "DimText")
    self.companionDescLabel = descLabel

    -- Bond label
    local bondLabel = page:CreateFontString(nil, "OVERLAY")
    -- Anchor below the description so a 3-line description can't overlap
    -- (equivalent to the old fixed -(INSET+44) for the typical 2-line case)
    bondLabel:SetPoint("TOPLEFT", descLabel, "BOTTOMLEFT", 0, -14)
    bondLabel:SetFont("Fonts\\FRIZQT__.TTF", 12, "")
    bondLabel:SetText("Bond:")
    bondLabel:SetTextColor(c.text_dim.r, c.text_dim.g, c.text_dim.b, 1)
    VC.Theme:Register(bondLabel, "DimText")

    -- Bond progress bar
    local BOND_BAR_W = 160
    local bondBarBg = page:CreateTexture(nil, "ARTWORK")
    bondBarBg:SetSize(BOND_BAR_W, 10)
    bondBarBg:SetPoint("LEFT", bondLabel, "RIGHT", 6, 0)
    bondBarBg:SetTexture(WHITE)
    bondBarBg:SetVertexColor(c.bg.r, c.bg.g, c.bg.b, 1)

    local bondBarFill = page:CreateTexture(nil, "OVERLAY")
    bondBarFill:SetSize(1, 10)
    bondBarFill:SetPoint("LEFT", bondBarBg, "LEFT", 0, 0)
    bondBarFill:SetTexture(WHITE)
    bondBarFill:SetVertexColor(c.accent.r, c.accent.g, c.accent.b, 0.8)
    VC.Theme:Register(bondBarFill, "AccentText")
    self.bondBarFill = bondBarFill
    self.bondBarW = BOND_BAR_W

    local bondValueLabel = page:CreateFontString(nil, "OVERLAY")
    bondValueLabel:SetPoint("TOPLEFT", bondBarBg, "BOTTOMLEFT", 0, -3)
    bondValueLabel:SetFont("Fonts\\FRIZQT__.TTF", 12, "")
    bondValueLabel:SetTextColor(c.accent.r, c.accent.g, c.accent.b, 1)
    VC.Theme:Register(bondValueLabel, "AccentText")
    self.bondValueLabel = bondValueLabel

    -- Signature quote (legendary tagline per companion)
    local sigQuote = page:CreateFontString(nil, "OVERLAY")
    sigQuote:SetPoint("TOPLEFT", bondValueLabel, "BOTTOMLEFT", 0, -8)
    sigQuote:SetPoint("RIGHT", page, "RIGHT", -INSET, 0)
    sigQuote:SetFont("Fonts\\FRIZQT__.TTF", 11, "")
    sigQuote:SetTextColor(c.text_dim.r, c.text_dim.g, c.text_dim.b, 0.7)
    sigQuote:SetJustifyH("LEFT")
    sigQuote:SetWordWrap(true)
    sigQuote:SetMaxLines(3)
    VC.Theme:Register(sigQuote, "DimText")
    self.sigQuote = sigQuote

    -- TTS voice label (below quote)
    local ttsVoiceLabel = page:CreateFontString(nil, "OVERLAY")
    ttsVoiceLabel:SetPoint("TOPLEFT", sigQuote, "BOTTOMLEFT", 0, -4)
    ttsVoiceLabel:SetFont("Fonts\\FRIZQT__.TTF", 10, "")
    ttsVoiceLabel:SetTextColor(c.text_dim.r, c.text_dim.g, c.text_dim.b, 0.5)
    ttsVoiceLabel:SetJustifyH("LEFT")
    VC.Theme:Register(ttsVoiceLabel, "DimText")
    self.ttsVoiceLabel = ttsVoiceLabel

    -- =========================================================================
    -- Linked Battle Pet (right column, below sigQuote)
    -- =========================================================================
    local LINKED_PET_Y = -160
    local lpSep = page:CreateTexture(nil, "ARTWORK")
    lpSep:SetHeight(1)
    lpSep:SetPoint("TOPLEFT", RIGHT_COL_X, LINKED_PET_Y)
    lpSep:SetPoint("RIGHT", page, "RIGHT", -INSET, 0)
    lpSep:SetTexture(WHITE); lpSep:SetVertexColor(c.border.r, c.border.g, c.border.b, 0.4)
    lpSep._alpha = 0.4; VC.Theme:Register(lpSep, "Separator")

    local lpHeader = page:CreateFontString(nil, "OVERLAY")
    lpHeader:SetPoint("TOPLEFT", RIGHT_COL_X, LINKED_PET_Y - 10)
    lpHeader:SetFont("Fonts\\FRIZQT__.TTF", 12, "")
    lpHeader:SetText("LINKED BATTLE PET")
    lpHeader:SetTextColor(c.text_dim.r, c.text_dim.g, c.text_dim.b, 1)
    VC.Theme:Register(lpHeader, "DimText")

    -- Pet display row (icon + name + level + type)
    local lpRow = CreateFrame("Frame", nil, page, "BackdropTemplate")
    lpRow:SetSize(W - RIGHT_COL_X - INSET, 40)
    lpRow:SetPoint("TOPLEFT", RIGHT_COL_X, LINKED_PET_Y - 28)
    lpRow:SetBackdrop({ bgFile = WHITE, edgeFile = WHITE, edgeSize = 1 })
    lpRow:SetBackdropColor(0, 0, 0, 0.15)
    lpRow:SetBackdropBorderColor(c.border.r, c.border.g, c.border.b, 0.3)
    lpRow:EnableMouse(true)
    self._linkedPetRow = lpRow

    local lpIcon = lpRow:CreateTexture(nil, "ARTWORK")
    lpIcon:SetSize(32, 32)
    lpIcon:SetPoint("LEFT", 4, 0)
    lpIcon:SetTexture("Interface\\Icons\\INV_Misc_QuestionMark")
    self._linkedPetIcon = lpIcon

    local lpName = lpRow:CreateFontString(nil, "OVERLAY")
    lpName:SetPoint("TOPLEFT", lpIcon, "TOPRIGHT", 6, -2)
    lpName:SetPoint("RIGHT", lpRow, "RIGHT", -6, 0)
    lpName:SetFont("Fonts\\FRIZQT__.TTF", 12, "")
    lpName:SetJustifyH("LEFT")
    lpName:SetTextColor(c.text.r, c.text.g, c.text.b, 1)
    VC.Theme:Register(lpName, "Text")
    self._linkedPetName = lpName

    local lpInfo = lpRow:CreateFontString(nil, "OVERLAY")
    lpInfo:SetPoint("BOTTOMLEFT", lpIcon, "BOTTOMRIGHT", 6, 2)
    lpInfo:SetFont("Fonts\\FRIZQT__.TTF", 11, "")
    lpInfo:SetJustifyH("LEFT")
    lpInfo:SetTextColor(c.text_dim.r, c.text_dim.g, c.text_dim.b, 0.7)
    VC.Theme:Register(lpInfo, "DimText")
    self._linkedPetInfo = lpInfo

    -- Buttons
    local lpChoose = VC.UI:CreatePillButton(page, "Choose...", 80, 22, function()
        self:OpenPetSelector()
    end)
    lpChoose:SetPoint("TOPLEFT", RIGHT_COL_X, LINKED_PET_Y - 72)
    self._linkedPetChooseBtn = lpChoose

    local lpClear = VC.UI:CreatePillButton(page, "Clear", 60, 22, function()
        local key = self._editingKey
        if key then
            VC.Store:Dispatch("SET_LINKED_PET", { companionKey = key, petGUID = nil })
            self:RefreshLinkedPet(key)
        end
    end)
    lpClear:SetPoint("LEFT", lpChoose, "RIGHT", 6, 0)
    self._linkedPetClearBtn = lpClear

    -- Click empty row to open selector
    lpRow:SetScript("OnMouseDown", function()
        local state = VC.Store:GetState()
        local key = self._editingKey
        if key and not state.config.linkedPets[key] then
            self:OpenPetSelector()
        end
    end)

    -- =========================================================================
    -- Model variant grid (full-width below both columns)
    -- =========================================================================
    local GRID_SEP_Y = -310
    local gridSep = page:CreateTexture(nil, "ARTWORK")
    gridSep:SetHeight(1)
    gridSep:SetPoint("TOPLEFT", 0, GRID_SEP_Y)
    gridSep:SetPoint("TOPRIGHT", 0, GRID_SEP_Y)
    gridSep:SetTexture(WHITE)
    gridSep:SetVertexColor(c.border.r, c.border.g, c.border.b, 0.4)
    gridSep._alpha = 0.4; VC.Theme:Register(gridSep, "Separator")

    local modelLabel = page:CreateFontString(nil, "OVERLAY")
    modelLabel:SetPoint("TOPLEFT", INSET, GRID_SEP_Y - 12)
    modelLabel:SetFont("Fonts\\FRIZQT__.TTF", 12, "")
    modelLabel:SetText("ALTERNATE APPEARANCES")
    modelLabel:SetTextColor(c.text_dim.r, c.text_dim.g, c.text_dim.b, 1)
    VC.Theme:Register(modelLabel, "DimText")
    self._modelLabel = modelLabel

    local CARD_SIZE = 64
    local CARD_PAD = 5
    local COLS = math.floor((W - INSET * 2) / (CARD_SIZE + CARD_PAD))

    local modelScroll = CreateFrame("ScrollFrame", nil, page, "UIPanelScrollFrameTemplate")
    modelScroll:SetPoint("TOPLEFT", INSET, GRID_SEP_Y - 28)
    modelScroll:SetPoint("BOTTOMRIGHT", -INSET, 4)
    local modelContent = CreateFrame("Frame", nil, modelScroll)
    modelContent:SetWidth(W - INSET * 2 - 20)
    modelContent:SetHeight(1)
    modelScroll:SetScrollChild(modelContent)
    modelScroll:EnableMouseWheel(true)
    modelScroll:SetScript("OnMouseWheel", function(self, delta)
        local cur = self:GetVerticalScroll()
        local maxS = math.max(0, modelContent:GetHeight() - self:GetHeight())
        self:SetVerticalScroll(math.max(0, math.min(maxS, cur - delta * 40)))
    end)
    VC.UI.StyleScrollBar(modelScroll)

    self._modelScroll = modelScroll
    self._modelContent = modelContent
    self._modelCards = {}
    self._CARD_SIZE = CARD_SIZE
    self._CARD_PAD = CARD_PAD
    self._CARD_COLS = COLS
end

-- ============================================================================
-- REFRESH COMPANION PAGE
-- ============================================================================

function VC.Settings:RefreshCompanionPage(companionKey)
    local c = VC.Theme:GetScheme()
    local profile = VC_CompanionProfiles and VC_CompanionProfiles[companionKey]
    if not profile then return end

    -- Preview model
    if self.previewScene then
        local state = VC.Store:GetState()
        local override = state.config.modelOverrides and state.config.modelOverrides[companionKey]
        local displayID = override or profile.displayID or 19598
        VC.UI:SetCreatureOnScene(self.previewScene, displayID, { scale = 0.8 })
    end

    -- Preview mood ring
    if self.previewMoodRing and VC.Mood then
        local mood = VC.Mood:GetMood()
        local mc = VC.Constants.MOOD_COLORS[mood] or VC.Constants.MOOD_COLORS.NEUTRAL
        self.previewMoodRing:SetVertexColor(mc.r, mc.g, mc.b, 0.8)
    end

    -- Update preview frame border to companion themeColor
    if self.previewFrame and profile.themeColor then
        local tc = profile.themeColor
        self.previewFrame:SetBackdropBorderColor(tc.r, tc.g, tc.b, 0.7)
    end

    -- Name + title + description
    if self.companionNameLabel then self.companionNameLabel:SetText(profile.name) end
    if self.companionTitleLabel then self.companionTitleLabel:SetText(profile.title or "") end
    if self.companionDescLabel then self.companionDescLabel:SetText(profile.description or "") end
    if self.sigQuote then self.sigQuote:SetText(profile.sigQuote or "") end
    if self.ttsVoiceLabel then
        self.ttsVoiceLabel:SetText(profile.ttsVoice and ("TTS: " .. profile.ttsVoice) or "")
    end

    -- Bond bar
    if self.bondBarFill and VC.Bonding then
        local info = VC.Bonding:GetLevelInfo(companionKey)
        if info then
            local pct = 0
            if info.nextThreshold and info.nextThreshold > 0 then
                local levels = VC.Constants.BONDING.LEVELS
                local prevThresh = 0
                for _, lvl in ipairs(levels) do
                    if lvl.name == info.name then break end
                    prevThresh = lvl.threshold
                end
                local span = info.nextThreshold - prevThresh
                if span > 0 then pct = math.max(0, math.min(1, (info.rapport - prevThresh) / span)) end
            else
                pct = 1
            end
            local fillW = math.max(1, math.floor((self.bondBarW or 160) * pct))
            self.bondBarFill:SetWidth(fillW)
            local next = info.nextThreshold and tostring(info.nextThreshold) or "Max"
            if self.bondValueLabel then
                self.bondValueLabel:SetText(info.name .. " " .. info.rapport .. "/" .. next)
            end
        end
    end

    -- Mood dot + state name
    if self.moodDot and VC.Mood then
        local mood = VC.Mood:GetMood()
        local mc = VC.Constants.MOOD_COLORS[mood] or VC.Constants.MOOD_COLORS.NEUTRAL
        self.moodDot:SetVertexColor(mc.r, mc.g, mc.b, 1)
        if self.moodStateLabel then
            self.moodStateLabel:SetText(mood:sub(1,1) .. mood:sub(2):lower())
            self.moodStateLabel:SetTextColor(mc.r, mc.g, mc.b, 1)
        end
    end

    -- Model variant grid (full rebuild only on companion change; highlight
    -- repaint always, so an external SET_MODEL_OVERRIDE moves the active ring)
    if self._lastGridKey ~= companionKey then
        self:RefreshModelGrid(companionKey)
        self._lastGridKey = companionKey
    else
        self:RefreshModelGrid(companionKey, true)
    end

    self:RefreshLinkedPet(companionKey)
end

-- ============================================================================
-- MODEL GRID REFRESH
-- ============================================================================

function VC.Settings:RefreshModelGrid(companionKey, highlightOnly)
    local content = self._modelContent
    if not content then return end
    local c = VC.Theme:GetScheme()
    local profile = VC_CompanionProfiles and VC_CompanionProfiles[companionKey]
    local models = profile and profile.models

    if not highlightOnly then
        for _, card in ipairs(self._modelCards or {}) do
            card:Hide()
        end
    end

    if not models or #models == 0 then
        if self._modelLabel then self._modelLabel:SetText("ALTERNATE APPEARANCES") end
        content:SetHeight(1)
        return
    end

    if not highlightOnly and self._modelLabel then
        self._modelLabel:SetText("ALTERNATE APPEARANCES (" .. #models .. ")")
    end

    local state = VC.Store:GetState()
    local currentOverride = state.config.modelOverrides[companionKey]
    local activeDisplayID = currentOverride or profile.displayID

    local CARD_SIZE = self._CARD_SIZE
    local PAD = self._CARD_PAD
    local COLS = self._CARD_COLS

    for i, displayID in ipairs(models) do
        local card = self._modelCards[i]

        if not highlightOnly then
            if not card then
                card = CreateFrame("Frame", nil, content, "BackdropTemplate")
                card:SetSize(CARD_SIZE, CARD_SIZE)
                card:SetBackdrop(VC.Theme.BACKDROP_INSET)
                card:EnableMouse(true)

                local cardScene = VC.UI:CreateModelScene(card)
                cardScene:SetPoint("TOPLEFT", 2, -2)
                cardScene:SetPoint("BOTTOMRIGHT", -2, 2)
                cardScene:EnableMouse(false)  -- pass clicks through to card
                card._scene = cardScene

                self._modelCards[i] = card
            end

            local col = (i - 1) % COLS
            local row = math.floor((i - 1) / COLS)
            card:ClearAllPoints()
            card:SetPoint("TOPLEFT", col * (CARD_SIZE + PAD), -(row * (CARD_SIZE + PAD)))

            VC.UI:SetCreatureOnScene(card._scene, displayID, { scale = 0.9 })

            local did = displayID
            local cKey = companionKey
            card:SetScript("OnMouseDown", function()
                if InCombatLockdown() then return end
                VC.Store:Dispatch("SET_MODEL_OVERRIDE", { companionKey = cKey, displayID = did })
                if VC.Portrait and VC.Portrait.UpdateModel then VC.Portrait:UpdateModel() end
                if self.previewScene then
                    VC.UI:SetCreatureOnScene(self.previewScene, did, { scale = 0.8 })
                end
                self:RefreshModelGrid(cKey, true)
            end)

            card:Show()
        end

        -- Always update highlight
        if card then
            if displayID == activeDisplayID then
                card:SetBackdropColor(c.accent.r, c.accent.g, c.accent.b, 0.2)
                card:SetBackdropBorderColor(c.accent.r, c.accent.g, c.accent.b, 1)
            else
                card:SetBackdropColor(c.panel.r, c.panel.g, c.panel.b, 1)
                card:SetBackdropBorderColor(c.border.r, c.border.g, c.border.b, 1)
            end
        end
    end

    local totalRows = math.ceil(#models / COLS)
    content:SetHeight(totalRows * (CARD_SIZE + PAD) + PAD)
end

-- ============================================================================
-- LINKED BATTLE PET
-- ============================================================================

local PET_TYPE_NAMES = {
    [1]="Humanoid", [2]="Dragonkin", [3]="Flying", [4]="Undead", [5]="Critter",
    [6]="Magic", [7]="Elemental", [8]="Beast", [9]="Aquatic", [10]="Mechanical",
}

function VC.Settings:RefreshLinkedPet(companionKey)
    if not self._linkedPetRow then return end
    local state = VC.Store:GetState()
    local petGUID = state.config.linkedPets[companionKey]
    local c = VC.Theme:GetScheme()

    if petGUID and C_PetJournal then
        local speciesID, customName, level, _, _, _, _, name, icon, petType = C_PetJournal.GetPetInfoByPetID(petGUID)
        if speciesID then
            self._linkedPetIcon:SetTexture(icon or "Interface\\Icons\\INV_Misc_QuestionMark")
            self._linkedPetName:SetText(customName or name or "Unknown")
            local typeName = PET_TYPE_NAMES[petType] or ""
            self._linkedPetInfo:SetText("Lv " .. (level or "?") .. "  " .. typeName)
            self._linkedPetRow:SetBackdropBorderColor(c.border.r, c.border.g, c.border.b, 0.5)
            if self._linkedPetClearBtn then self._linkedPetClearBtn:Show() end
        else
            -- Pet was caged or released
            self._linkedPetIcon:SetTexture("Interface\\Icons\\INV_Misc_QuestionMark")
            self._linkedPetName:SetText("Pet not found")
            self._linkedPetInfo:SetText("Caged or released -- click Clear")
            self._linkedPetRow:SetBackdropBorderColor(0.6, 0.2, 0.2, 0.5)
            if self._linkedPetClearBtn then self._linkedPetClearBtn:Show() end
        end
    else
        -- No pet linked — show empty state
        self._linkedPetIcon:SetTexture("Interface\\PaperDollInfoFrame\\Character-Plus")
        self._linkedPetName:SetText("No pet linked")
        self._linkedPetName:SetTextColor(c.text_dim.r, c.text_dim.g, c.text_dim.b, 0.7)
        self._linkedPetInfo:SetText("Click to choose")
        self._linkedPetRow:SetBackdropBorderColor(c.border.r, c.border.g, c.border.b, 0.2)
        if self._linkedPetClearBtn then self._linkedPetClearBtn:Hide() end
    end
end

function VC.Settings:OpenPetSelector()
    if not C_PetJournal then return end
    local companionKey = self._editingKey
    if not companionKey then return end

    local c = VC.Theme:GetScheme()
    local popup = self._petPopup or self:_BuildPetPopup()
    popup._companionKey = companionKey
    popup:SetBackdropColor(c.panel.r, c.panel.g, c.panel.b, 0.95)
    popup:SetBackdropBorderColor(c.accent.r, c.accent.g, c.accent.b, 0.8)

    -- Position near the linked pet row
    popup:ClearAllPoints()
    local row = self._linkedPetRow
    if row then
        popup:SetPoint("TOPLEFT", row, "BOTTOMLEFT", 0, -4)
    else
        popup:SetPoint("CENTER")
    end

    -- Snapshot the journal once per open; the search filter works off this.
    -- Summoned GUID read once here too (known ~1s nil window post-summon;
    -- per-keystroke reads in _rebuild just made the pin flicker)
    popup._petData = self:_FetchOwnedPetData()
    popup._summoned = C_PetJournal.GetSummonedPetGUID()
    popup._capture:Show()
    popup._search:SetText("")
    popup:Show()
    popup._rebuild("")
end

-- Build-once popup chrome (the old per-open CreateFrame leaked a popup +
-- fullscreen capture frame and stacked an OnHide hook on every open)
function VC.Settings:_BuildPetPopup()
    local popup = CreateFrame("Frame", "VC_PetSelectorPopup", UIParent, "BackdropTemplate")
    popup:SetSize(300, 320)
    popup:EnableMouse(true) -- without this, clicks in backdrop gaps fall through to the capture frame and dismiss the popup
    popup:SetFrameStrata("DIALOG")
    popup:SetBackdrop({ bgFile = WHITE, edgeFile = WHITE, edgeSize = 1 })
    popup:SetClampedToScreen(true)
    tinsert(UISpecialFrames, "VC_PetSelectorPopup")
    self._petPopup = popup

    -- Click-outside capture frame
    local capture = CreateFrame("Frame", nil, UIParent)
    capture:SetAllPoints(UIParent)
    capture:SetFrameStrata("HIGH")
    capture:EnableMouse(true)
    capture:SetScript("OnMouseDown", function() popup:Hide() end)
    capture:Hide()
    popup._capture = capture
    popup:SetScript("OnHide", function() capture:Hide() end)

    -- Search box
    local search = CreateFrame("EditBox", nil, popup, "InputBoxTemplate")
    search:SetSize(268, 20)
    search:SetPoint("TOPLEFT", 16, -12)
    search:SetAutoFocus(true)
    search:SetFont("Fonts\\FRIZQT__.TTF", 11, "")
    popup._search = search

    -- Virtualized pet list -- owned journals run 1000+ pets, only the
    -- ~11 visible rows ever exist as frames
    local ROW_H = 28
    local scrollBox = CreateFrame("Frame", nil, popup, "WowScrollBoxList")
    scrollBox:SetPoint("TOPLEFT", 8, -40)
    scrollBox:SetPoint("BOTTOMRIGHT", -30, 8)
    local scrollBar = CreateFrame("EventFrame", nil, popup, "MinimalScrollBar")
    scrollBar:SetPoint("TOPLEFT", scrollBox, "TOPRIGHT", 8, 0)
    scrollBar:SetPoint("BOTTOMLEFT", scrollBox, "BOTTOMRIGHT", 8, 0)
    local view = CreateScrollBoxListLinearView()
    view:SetElementExtent(ROW_H)
    view:SetElementInitializer("Button", function(row, pet)
        self:_InitPetRow(row, pet, popup)
    end)
    ScrollUtil.InitScrollBoxListWithScrollBar(scrollBox, scrollBar, view)
    popup._scrollBox = scrollBox

    popup._rebuild = function(filter)
        local filtered = {}
        if filter and filter ~= "" then
            local lf = filter:lower()
            for _, p in ipairs(popup._petData) do
                if p.name and p.name:lower():find(lf, 1, true) then
                    filtered[#filtered + 1] = p
                end
            end
        else
            for _, p in ipairs(popup._petData) do filtered[#filtered + 1] = p end
        end

        -- Pin currently summoned pet at top (_summoned snapshotted at open)
        if popup._summoned then
            for i, p in ipairs(filtered) do
                if p.guid == popup._summoned then
                    table.remove(filtered, i)
                    table.insert(filtered, 1, p)
                    break
                end
            end
        end

        scrollBox:SetDataProvider(CreateDataProvider(filtered))
    end

    search:SetScript("OnTextChanged", function(box)
        popup._rebuild(box:GetText())
    end)
    search:SetScript("OnEscapePressed", function()
        popup:Hide()
    end)

    -- Close popup when Settings closes
    if self.frame then
        self.frame:HookScript("OnHide", function() popup:Hide() end)
    end

    return popup
end

function VC.Settings:_FetchOwnedPetData()
    local petData = {}
    local allPets = C_PetJournal.GetOwnedPetIDs() or {} -- exception(boundary): nil before journal init
    for _, guid in ipairs(allPets) do
        local sid, cName, lvl, _, _, _, isFav, pName, ico, pType = C_PetJournal.GetPetInfoByPetID(guid)
        if sid then
            petData[#petData + 1] = {
                guid = guid, name = cName or pName, icon = ico,
                level = lvl, petType = pType, favorite = isFav,
            }
        end
    end
    table.sort(petData, function(a, b) return (a.name or "") < (b.name or "") end)
    return petData
end

function VC.Settings:_InitPetRow(row, pet, popup)
    local c = VC.Theme:GetScheme()
    if not row._built then
        row._built = true
        row:SetHighlightTexture(WHITE)

        row._icon = row:CreateTexture(nil, "ARTWORK")
        row._icon:SetSize(22, 22)
        row._icon:SetPoint("LEFT", 2, 0)

        row._name = row:CreateFontString(nil, "OVERLAY")
        row._name:SetPoint("LEFT", row._icon, "RIGHT", 4, 0)
        row._name:SetPoint("RIGHT", row, "RIGHT", -50, 0)
        row._name:SetFont("Fonts\\FRIZQT__.TTF", 11, "")
        row._name:SetJustifyH("LEFT")

        row._info = row:CreateFontString(nil, "OVERLAY")
        row._info:SetPoint("RIGHT", -4, 0)
        row._info:SetFont("Fonts\\FRIZQT__.TTF", 10, "")
        row._info:SetJustifyH("RIGHT")
    end
    row:GetHighlightTexture():SetVertexColor(c.accent.r, c.accent.g, c.accent.b, 0.15)
    row._name:SetTextColor(c.text.r, c.text.g, c.text.b, 1)
    row._info:SetTextColor(c.text_dim.r, c.text_dim.g, c.text_dim.b, 0.7)

    row._icon:SetTexture(pet.icon or "Interface\\Icons\\INV_Misc_QuestionMark")
    row._name:SetText(pet.name or "?")
    local typeName = PET_TYPE_NAMES[pet.petType] or ""
    local prefix = (popup._summoned and pet.guid == popup._summoned) and "|cFF80FF80*|r " or ""
    row._info:SetText(prefix .. "Lv" .. (pet.level or "?") .. " " .. typeName)

    row:SetScript("OnClick", function()
        VC.Store:Dispatch("SET_LINKED_PET", { companionKey = popup._companionKey, petGUID = pet.guid })
        self:RefreshLinkedPet(popup._companionKey)
        -- Summon handled by EventBus (SET_LINKED_PET -> PetSummoner reconcile)
        popup:Hide()
    end)
end
