local ADDON_NAME, ns = ...

local GuideView = {}
ns.GuideView = GuideView

local SECTION_DEFS = {
    { id = "overview", title = "Overview", icon = "icons/overview" },
    { id = "map", title = "Map & Buffs", icon = "icons/map" },
    { id = "strategy", title = "Strategy", icon = "icons/strategy" },
    { id = "roles", title = "Role Responsibilities", icon = "icons/role_responsibilities" },
    { id = "tips", title = "Tips & Mistakes", icon = "icons/advanced_tip" },
}

-- Objectives glance only - Common Mistake lives under Tips & Mistakes, not here.
-- matchLength stays on the hero/map meta line, not duplicated under Objectives.
local GLANCE_ORDER = {
    { key = "winCondition", label = "Win Condition" },
    { key = "primaryObjective", label = "Primary Objective" },
    { key = "openingFocus", label = "Opening Focus" },
}

local function BuildLookup()
    if ns.GuideBySlug then return end
    ns.GuideBySlug = {}
    if ns.Guides then
        for _, g in ipairs(ns.Guides) do ns.GuideBySlug[g.slug] = g end
    end
end
ns.GuideView.BuildLookup = BuildLookup

local function meta(slug) return ns.GuideMeta and ns.GuideMeta[slug] end
local function content(slug) return ns.GuideContent and ns.GuideContent[slug] end

local function HasSection(slug, id)
    local m = meta(slug)
    local c = content(slug)
    if id == "overview" then return true end
    if id == "map" then return true end
    if id == "strategy" then
        return (m and (m.openingSubsections or m.standardSubsections or m.rotationsSubsections
            or m.rotationFlow or m.midSubsections or m.lateSubsections))
            or (c and (c.opening or c.standard or c.rotations or c.mid or c.late))
    end
    if id == "roles" then return (m and m.roleCards) or (c and c.roles) end
    if id == "tips" then
        return (m and (m.mistakesSubsections or m.advancedSubsections))
            or (c and (c.mistakes or c.advanced))
    end
    return false
end

-- sectionTitle: when set, hide titles that repeat the collapsible header.
-- Tips sections (Common Mistakes / Advanced Tips) stay flat under that header -
-- no second ConceptBlock icon/gap. Applies to every map via StratSection.
-- Also suppress a card title that repeats its subsection title (e.g. AB
-- Opening -> "Opening Split" / "Opening Split").
local function RenderSubsections(b, subs, defaultComponent, sectionTitle)
    local sec = sectionTitle and sectionTitle:lower() or nil
    local flatTips = sec == "common mistakes" or sec == "advanced tips"
    for i, sub in ipairs(subs) do
        if sub.dividerBefore then
            -- Equal air above/below the bar. Subheading adds GAP_GROUP (6) after
            -- the below spacer, so pad below is 4 -> 10 total to the title.
            local gap = 10
            b:Spacer(gap)
            b:Rule({ section = true, bare = true })
            b:Spacer(gap - 6)
        end
        local subTitle = sub.title and sub.title:lower() or nil
        if sub.title and (not sec or subTitle ~= sec) then
            b:Subheading(sub.title)
        end
        if sub.bullets then b:Bullets(sub.bullets) end
        if sub.cards then
            local n = #sub.cards
            for j, card in ipairs(sub.cards) do
                local cardTitle = card.title and card.title:lower() or nil
                local dupTitle = (sec and cardTitle == sec)
                    or (subTitle and cardTitle == subTitle)
                if card.url and card.url ~= "" then
                    b:Card({
                        type = card.type,
                        title = dupTitle and nil or card.title,
                        body = card.body,
                        bullets = card.bullets,
                        link = card.url,
                        linkLabel = card.linkLabel,
                    })
                elseif flatTips or dupTitle then
                    -- Collapsible / subsection already carries this title.
                    if card.title and not dupTitle then
                        b:Subheading(card.title)
                    end
                    if card.body and card.body ~= "" then b:Paragraph(card.body) end
                    if card.bullets then b:Bullets(card.bullets) end
                else
                    b:ConceptBlock({
                        component = card.component or defaultComponent,
                        type = card.type,
                        title = card.title,
                        body = card.body,
                        bullets = card.bullets,
                        last = (j == n),
                    })
                end
            end
        end
        local nxt = subs[i + 1]
        if not (nxt and nxt.dividerBefore) then
            b:Spacer(6)
        end
    end
end

local function RenderParagraphs(b, list)
    for _, p in ipairs(list) do b:Paragraph(p) end
end

-- Intro paragraph, rendered plainly like an Adventure Guide boss description.
local function RenderOverviewIntro(b, slug)
    local g = ns.GuideBySlug and ns.GuideBySlug[slug]
    if g and g.overview then b:Paragraph(g.overview) end
end

-- Non-addon overview cards only (map notes, cadence, scoring). Addon install
-- cards live under Overview -> Addons via ns.GuideAddonRecs.
local ADDON_CARD_TITLES = {
    ["BattlegroundEnemiesFixed"] = true,
    ["BG Announcer"] = true,
    ["Follow the Flag"] = true,
    ["Trigzy SSM Cart Timers"] = true,
}

local function AddonByName(name)
    if not ns.Addons then return nil end
    for _, a in ipairs(ns.Addons) do
        if a.name == name then return a end
    end
    return nil
end

local function RenderObjectivesBody(b, slug)
    local m = meta(slug)
    local g = ns.GuideBySlug and ns.GuideBySlug[slug]
    if m and m.glance then
        b:KeyValues(m.glance, GLANCE_ORDER)
    end
    if g and g.overviewCards then
        local first = true
        for _, card in ipairs(g.overviewCards) do
            if not ADDON_CARD_TITLES[card.title] then
                if first then b:Spacer(6); first = false end
                b:Card({
                    type = card.type,
                    title = card.title,
                    body = card.body,
                    bullets = card.bullets,
                    link = card.url,
                    linkLabel = card.linkLabel,
                })
            end
        end
    end
end

local function RenderAddonsBody(b, slug)
    local recs = ns.GuideAddonRecs and ns.GuideAddonRecs[slug]
    if not recs or #recs == 0 then return end
    for i, rec in ipairs(recs) do
        if i > 1 then b:Rule({ section = true }) end
        local hub = AddonByName(rec.name)
        b:Card({
            type = "rec",
            title = rec.name,
            bullets = rec.bullets,
            link = hub and hub.url,
            linkLabel = "CurseForge",
        })
    end
end

local function RenderRolesBody(b, slug)
    local m = meta(slug)
    local roleFilter = ns.GuideView and ns.GuideView.role
    if m and m.roleCards then
        for _, roleKey in ipairs(ns.UI.ROLE_ORDER) do
            local role = m.roleCards[roleKey]
            if role and (not roleFilter or roleFilter == "general" or roleFilter == roleKey) then
                b:Subheading(ns.UI.ROLE_TITLES[roleKey] or roleKey)
                for _, bucket in ipairs(ns.UI.ROLE_BUCKETS) do
                    local list = role[bucket.key]
                    if list and #list > 0 then
                        b:Card({ type = bucket.type, title = bucket.label, bullets = list })
                    end
                end
                if role.specs then
                    for _, spec in ipairs(role.specs) do
                        local title = (spec.spec or "") .. (spec.title and (" - " .. spec.title) or "")
                        b:Card({ type = "advanced", title = title, bullets = spec.bullets })
                    end
                end
                b:Spacer(8)
            end
        end
    else
        local c = content(slug)
        if c and c.roles then b:Bullets(c.roles) end
    end
end

-- Default component per strategy phase, so authored cards without an explicit
-- type still render as the right library component.
local SECTION_COMPONENT = {
    opening = "opening", rotations = "rotation", mistakes = "warning",
    advanced = "advanced",
}

local function RenderStrategyBody(b, slug, id, metaKey, contentKey, asBullets, sectionTitle)
    local m = meta(slug)
    local c = content(slug)
    if id == "rotations" and m and m.rotationFlow then
        b:Flow(m.rotationFlow)
    end
    if m and m[metaKey] then
        RenderSubsections(b, m[metaKey], SECTION_COMPONENT[id], sectionTitle)
    elseif c and c[contentKey] then
        if asBullets then b:Bullets(c[contentKey]) else RenderParagraphs(b, c[contentKey]) end
    end
end

-- Battleground artwork tiles -------------------------------------------------
-- Shared tile widget used by the browser grid and the "related guides" strip.

-- Blizzard's dungeon tiles are 174x96; keep the same proportions.
local TILE_ASPECT = 96 / 174

-- Build tiles from Blizzard's own Adventure Guide instance button so they are
-- visually identical to the Dungeon browser (gold beveled frame, Morpheus
-- title, native hover/press states).
local function CreateTile(parent)
    local t = CreateFrame("Button", nil, parent, "EncounterInstanceButtonTemplate")
    if t.name then
        t.name:SetTextColor(1, 0.82, 0)
    end
    return t
end

local function LayoutTile(t, g, tileW, tileH)
    local screen = ns.GetLoadingScreen and ns.GetLoadingScreen(g.slug)
    if screen and t.bgImage then
        t.bgImage:SetTexture(screen)
        t.bgImage:SetTexCoord(0, 1, 0, 1)
        -- Keep the art inside the frame's beveled opening so bright corners
        -- don't bleed past the gold border.
        local ix = math.max(4, math.floor(tileW * 0.045))
        local iy = math.max(4, math.floor(tileH * 0.09))
        t.bgImage:ClearAllPoints()
        t.bgImage:SetPoint("TOPLEFT", ix, -iy)
        t.bgImage:SetPoint("BOTTOMRIGHT", -ix, iy)
    end
    if t.name then
        t.name:SetWidth(tileW - 24)
        t.name:SetText(g.name or g.slug)
    end
    -- Dungeon tiles show artwork + name only; hide the template's extra bits.
    if t.heroicIcon then t.heroicIcon:Hide() end
    if t.range then t.range:Hide() end
    if t.ModifiedInstanceIcon then t.ModifiedInstanceIcon:Hide() end
    t:Show()
end

-- Guide hero header (loading-screen art with overlaid title) -----------------

function GuideView:HideGuideHeader()
    if self.guideHeader then self.guideHeader:Hide() end
end

local function EnsureGuideHeader(self, parent)
    if self.guideHeader then return self.guideHeader end
    local h = CreateFrame("Frame", nil, parent, "BackdropTemplate")
    h:SetBackdrop({ edgeFile = "Interface\\Buttons\\WHITE8x8", edgeSize = 1 })
    h:SetBackdropBorderColor(0, 0, 0, 0.7)

    h.art = h:CreateTexture(nil, "BACKGROUND")
    h.art:SetPoint("TOPLEFT", 1, -1)
    h.art:SetPoint("BOTTOMRIGHT", -1, 1)

    -- Top gradient band so the gold title stays readable over bright artwork.
    h.shade = h:CreateTexture(nil, "BORDER")
    h.shade:SetPoint("TOPLEFT", 1, -1)
    h.shade:SetPoint("TOPRIGHT", -1, -1)
    h.shade:SetColorTexture(0, 0, 0, 0.55)

    -- Dark/off-white title (not gold) so the loading-screen art sells the BG.
    h.name = h:CreateFontString(nil, "OVERLAY", "GameFontNormalHuge")
    h.name:SetTextColor(0.95, 0.94, 0.92)
    h.name:SetShadowColor(0, 0, 0, 0.85)
    h.name:SetShadowOffset(1, -1)
    h.name:SetJustifyH("CENTER")
    h.name:SetPoint("TOP", 0, -10)

    h.meta = h:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    h.meta:SetTextColor(0.72, 0.72, 0.72)
    h.meta:SetShadowColor(0, 0, 0, 0.75)
    h.meta:SetShadowOffset(1, -1)
    h.meta:SetJustifyH("CENTER")
    h.meta:SetPoint("TOP", h.name, "BOTTOM", 0, -4)

    self.guideHeader = h
    return h
end

local function PlaceGuideHeader(self, parent, width, slug)
    local g = ns.GuideBySlug and ns.GuideBySlug[slug]
    local info = ns.GetMapInfo and ns.GetMapInfo(slug)
    local m = meta(slug)
    local h = EnsureGuideHeader(self, parent)

    local heroH = math.max(150, math.floor(width * 0.32))
    h:ClearAllPoints()
    h:SetPoint("TOPLEFT", 0, 0)
    h:SetSize(width, heroH)
    h:Show()

    local screen = ns.GetLoadingScreen and ns.GetLoadingScreen(slug)
    if screen then
        h.art:SetTexture(screen)
        h.art:SetTexCoord(0.04, 0.96, 0.06, 0.82)
    end
    h.shade:SetHeight(math.floor(heroH * 0.45))
    h.name:SetWidth(width - 32)
    h.name:SetText((g and g.name) or slug)

    local parts = { (info and info.mapType) or "Battleground" }
    if m and m.glance and m.glance.matchLength then
        -- Glance uses ** for parchment emphasis; strip on the dark hero so
        -- markers never show as literal asterisks (and dark-brown paint stays
        -- off this light-on-art line).
        parts[#parts + 1] = tostring(m.glance.matchLength):gsub("%*%*", "")
    end
    local role = ns.GuideView and ns.GuideView.role
    if role and role ~= "general" then
        parts[#parts + 1] = "|cff4dd24d" .. (ns.UI.ROLE_TITLES[role] or role) .. "|r"
    end
    h.meta:SetWidth(width - 32)
    h.meta:SetText(table.concat(parts, "   |cFF888888/|r   "))
    return heroH + 16
end

local ROLE_ICON = {
    tank = "inv_shield_06", healer = "spell_holy_flashheal",
    melee = "inv_sword_27", ranged = "inv_weapon_bow_07",
}

-- Collapsible section state (default COLLAPSED, like Blizzard's Adventure Guide
-- ability entries). Keyed by slug|chapter|section; only explicitly-expanded
-- sections are open.
function GuideView:ToggleSection(key)
    self.expanded = self.expanded or {}
    self.expanded[key] = not self.expanded[key]
    if self.currentChapter then
        self:RenderChapter(self.currentChapter, true)
    end
end

local function SectionIsCollapsed(gv, key)
    gv.expanded = gv.expanded or {}
    return gv.expanded[key] ~= true
end

-- Render a collapsible section as a Blizzard-style info box (icon tile + gold
-- title + chevron + bordered body panel).
local function Collapsible(b, gv, key, title, icon, renderFn)
    local collapsed = SectionIsCollapsed(gv, key)
    b:CollapsibleSection(key, title, icon, collapsed,
        function(k) gv:ToggleSection(k) end, renderFn)
end

-- A strategy phase, rendered as a collapsible section only if data exists.
local function StratSection(b, gv, slug, sid, title, icon, metaKey, contentKey, asBullets)
    local m = meta(slug)
    local c = content(slug)
    local has = (m and m[metaKey]) or (c and c[contentKey])
        or (sid == "rotations" and m and m.rotationFlow)
    if not has then return end
    Collapsible(b, gv, slug .. "|strategy|" .. sid, title, icon, function(bb)
        RenderStrategyBody(bb, slug, sid, metaKey, contentKey, asBullets, title)
    end)
end

local function RenderChapterBody(b, slug, id)
    local gv = ns.GuideView
    if id == "overview" then
        local m = meta(slug)
        RenderOverviewIntro(b, slug)
        if m and m.rememberThree then
            Collapsible(b, gv, slug .. "|overview|remember", "Key Points", "inv_misc_note_05", function(bb)
                bb:Bullets(m.rememberThree)
            end)
        end
        Collapsible(b, gv, slug .. "|overview|objectives", "Objectives", "inv_bannerpvp_02", function(bb)
            RenderObjectivesBody(bb, slug)
        end)
        if ns.GuideAddonRecs and ns.GuideAddonRecs[slug] then
            Collapsible(b, gv, slug .. "|overview|addons", "Addons", "inv_misc_enggizmos_17", function(bb)
                RenderAddonsBody(bb, slug)
            end)
        end
        if m and m.overviewSubsections then
            Collapsible(b, gv, slug .. "|overview|more", "More Detail", "inv_misc_book_11", function(bb)
                RenderSubsections(bb, m.overviewSubsections)
            end)
        end
    elseif id == "map" then
        ns.MapView:RenderEmbedded(b, slug)
        Collapsible(b, gv, slug .. "|map|buffs", "Map Legend", "icons/map", function(bb)
            ns.MapView:RenderLegend(bb, slug)
        end)
    elseif id == "strategy" then
        -- General Strategy first (scoring / win conditions), then Opening.
        StratSection(b, gv, slug, "standard", "General Strategy", "inv_sword_27", "standardSubsections", "standard", false)
        StratSection(b, gv, slug, "opening", "Opening", "inv_sword_04", "openingSubsections", "opening", false)
        StratSection(b, gv, slug, "rotations", "Rotations", "ability_hunter_pathfinding", "rotationsSubsections", "rotations", false)
        -- Temple combines mid/late into one section and does not surface a
        -- separate Late Game chapter.
        if slug == "temple-of-kotmogu" then
            StratSection(b, gv, slug, "mid", "Mid & Late Game", "spell_holy_borrowedtime", "midSubsections", "mid", false)
        else
            StratSection(b, gv, slug, "mid", "Mid Game", "spell_holy_borrowedtime", "midSubsections", "mid", false)
            StratSection(b, gv, slug, "late", "Late Game", "achievement_bg_winsoa", "lateSubsections", "late", false)
        end
    elseif id == "roles" then
        local m = meta(slug)
        local roleFilter = gv.role
        if m and m.roleCards then
            for _, roleKey in ipairs(ns.UI.ROLE_ORDER) do
                local role = m.roleCards[roleKey]
                if role and (not roleFilter or roleFilter == "general" or roleFilter == roleKey) then
                    Collapsible(b, gv, slug .. "|roles|" .. roleKey,
                        ns.UI.ROLE_TITLES[roleKey] or roleKey, ROLE_ICON[roleKey], function(bb)
                        for _, bucket in ipairs(ns.UI.ROLE_BUCKETS) do
                            local list = role[bucket.key]
                            if list and #list > 0 then
                                bb:Card({ type = bucket.type, title = bucket.label, bullets = list })
                            end
                        end
                        if role.specs then
                            for _, spec in ipairs(role.specs) do
                                local title = (spec.spec or "") .. (spec.title and (" - " .. spec.title) or "")
                                bb:Card({ type = "advanced", title = title, bullets = spec.bullets })
                            end
                        end
                    end)
                end
            end
        else
            local c = content(slug)
            if c and c.roles then b:Bullets(c.roles) end
        end
    elseif id == "tips" then
        StratSection(b, gv, slug, "mistakes", "Common Mistakes", "ability_creature_cursed_02", "mistakesSubsections", "mistakes", true)
        StratSection(b, gv, slug, "advanced", "Advanced Tips", "icons/advanced_tip", "advancedSubsections", "advanced", false)
    end
end

function GuideView:BuildChapters(slug)
    local chapters = {}
    for _, def in ipairs(SECTION_DEFS) do
        if HasSection(slug, def.id) then
            chapters[#chapters + 1] = { id = def.id, title = def.title, icon = def.icon }
        end
    end
    return chapters
end

function GuideView:HideMapButton()
    if self.mapBtn then self.mapBtn:Hide() end
end

-- Blizzard's "Show Map" button (map icon + label), overlaid on the hero art.
local function EnsureMapButton(self)
    if self.mapBtn then return self.mapBtn end
    local h = self.guideHeader
    local btn = CreateFrame("Button", nil, h)
    btn:SetSize(120, 34)

    btn.tex = btn:CreateTexture(nil, "ARTWORK")
    btn.tex:SetTexture("Interface\\QuestFrame\\UI-QuestMap_Button")
    btn.tex:SetTexCoord(0.125, 0.875, 0.0, 0.5)
    btn.tex:SetSize(46, 30)
    btn.tex:SetPoint("LEFT", 0, 0)

    btn.text = btn:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    btn.text:SetPoint("LEFT", btn.tex, "RIGHT", 2, 0)
    btn.text:SetShadowColor(0, 0, 0, 1)
    btn.text:SetShadowOffset(1, -1)

    local hl = btn:CreateTexture(nil, "HIGHLIGHT")
    hl:SetTexture("Interface\\BUTTONS\\ButtonHilight-Square")
    hl:SetBlendMode("ADD")
    hl:SetPoint("TOPLEFT", btn.tex, "TOPLEFT", 5, -3)
    hl:SetPoint("BOTTOMRIGHT", btn.tex, "BOTTOMRIGHT", -5, 3)

    btn:SetScript("OnMouseDown", function(s) s.tex:SetTexCoord(0.125, 0.875, 0.5, 1.0) end)
    btn:SetScript("OnMouseUp", function(s) s.tex:SetTexCoord(0.125, 0.875, 0.0, 0.5) end)

    self.mapBtn = btn
    return btn
end

local function ChapterTitle(id)
    for _, def in ipairs(SECTION_DEFS) do
        if def.id == id then return def.title end
    end
    return nil
end

-- When a role is selected, surface that role's relevant guidance at the top of
-- each strategy chapter (the role data lives in m.roleCards[role]).
local ROLE_CHAPTER_BUCKET = {
    overview = "primary", strategy = "primary", tips = "mistakes",
}

local function RenderRoleCallout(b, slug, chapterId)
    local role = ns.GuideView and ns.GuideView.role
    if not role or role == "general" then return end
    if chapterId == "roles" or chapterId == "map" then return end
    local m = meta(slug)
    local rc = m and m.roleCards and m.roleCards[role]
    if not rc then return end

    local key = ROLE_CHAPTER_BUCKET[chapterId]
    local list = key and rc[key]
    if not (list and #list > 0) then return end

    b:Card({
        type = (key == "mistakes") and "warn" or "rec",
        title = "As " .. (ns.UI.ROLE_TITLES[role] or role),
        bullets = list,
    })
    b:Spacer(4)
end

-- Render a single guide chapter (dungeon-encounter style: hero + one section).
function GuideView:RenderChapter(chapterId, keepScroll)
    local slug = self.currentSlug
    if not slug then return end
    BuildLookup()
    chapterId = chapterId or "overview"
    self.currentChapter = chapterId

    local mf = ns.MainFrame
    mf:EnterBookMode()
    mf:ClearContent()
    mf:SetSubnav()

    local width = mf:ContentWidth()
    local parent = mf.content

    local b = mf.builder
    b:Reset(width)
    b:SetTheme(true)

    if chapterId == "overview" then
        -- Only the Overview shows the hero artwork + Show Map button.
        local heroH = PlaceGuideHeader(self, parent, width, slug)
        local mapBtn = EnsureMapButton(self)
        mapBtn:SetParent(self.guideHeader)
        mapBtn:SetFrameLevel((self.guideHeader:GetFrameLevel() or 1) + 5)
        mapBtn:ClearAllPoints()
        mapBtn:SetPoint("BOTTOMLEFT", self.guideHeader, "BOTTOMLEFT", 12, 12)
        mapBtn.text:SetText("Show Map")
        mapBtn:SetScript("OnClick", function() ns.GuideView:SelectChapter("map") end)
        mapBtn:SetWidth(46 + (mapBtn.text:GetStringWidth() or 40) + 6)
        mapBtn:Show()
        b.y = heroH + 4
    else
        self:HideGuideHeader()
        self:HideMapButton()
        b.y = 0
        b:GuideTitle(ChapterTitle(chapterId) or "")
    end

    pcall(self.EnsureDifficultyDropdown, self, slug)

    RenderRoleCallout(b, slug, chapterId)
    RenderChapterBody(b, slug, chapterId)

    local endY = b:Finish()
    self:HideRelated()
    parent:SetHeight(math.max(endY + 16, 10))

    mf:ShowChapterSidebar(self:BuildChapters(slug), function(id)
        ns.GuideView:SelectChapter(id)
    end, chapterId)

    if not keepScroll then
        mf:ScrollToTop()
    end
end

-- Difficulty-style dropdown (top-right of the right page) for guide variants.
function GuideView:EnsureDifficultyDropdown(slug)
    local mf = ns.MainFrame
    self.role = self.role or "general"
    local m = meta(slug)

    local options = { { key = "general", text = "General" } }
    if m and m.roleCards then
        for _, rk in ipairs(ns.UI.ROLE_ORDER) do
            if m.roleCards[rk] then
                options[#options + 1] = { key = rk, text = ns.UI.ROLE_TITLES[rk] or rk }
            end
        end
    end

    local valid = false
    for _, o in ipairs(options) do if o.key == self.role then valid = true end end
    if not valid then self.role = "general" end

    local dd = self.diffDropdown
    if not dd then
        dd = CreateFrame("DropdownButton", nil, mf.frame, "WowStyle1DropdownTemplate")
        dd:SetWidth(150)
        self.diffDropdown = dd
    end
    dd:ClearAllPoints()
    dd:SetPoint("BOTTOMRIGHT", mf.scroll, "TOPRIGHT", 0, 3)
    dd:SetFrameLevel((mf.scroll:GetFrameLevel() or 1) + 10)

    dd:SetupMenu(function(_, root)
        for _, o in ipairs(options) do
            root:CreateRadio(o.text,
                function() return ns.GuideView.role == o.key end,
                function()
                    ns.GuideView.role = o.key
                    ns.GuideView:RenderChapter(ns.GuideView.currentChapter, true)
                end)
        end
    end)

    -- Only meaningful when a guide has role variants.
    dd:SetShown(#options > 1)
end

function GuideView:SelectChapter(id)
    if not self.currentSlug then return end
    self.chapterScroll = self.chapterScroll or {}
    if self.currentChapter then
        self.chapterScroll[self.currentChapter] = ns.MainFrame:GetScroll()
    end
    self:RenderChapter(id, true)
    ns.MainFrame:SetScroll(self.chapterScroll[id] or 0)
end

function GuideView:HideRelated()
    if self.relatedTiles then
        for _, t in ipairs(self.relatedTiles) do t:Hide() end
    end
    if self.relatedTitle then self.relatedTitle:Hide() end
    if self.relatedRule then self.relatedRule:Hide() end
end

function GuideView:RenderRelated(parent, width, slug, startY)
    self.relatedTiles = self.relatedTiles or {}
    for _, t in ipairs(self.relatedTiles) do t:Hide() end

    if not self.relatedTitle then
        self.relatedTitle = parent:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
        self.relatedTitle:SetTextColor(0.12, 0.08, 0.03)
        self.relatedRule = parent:CreateTexture(nil, "ARTWORK")
        self.relatedRule:SetColorTexture(0.30, 0.24, 0.16, 0.45)
        self.relatedRule:SetHeight(1)
    end

    local others = {}
    for _, g in ipairs(ns.Guides or {}) do
        if g.slug ~= slug then others[#others + 1] = g end
    end
    local n = math.min(4, #others)
    if n == 0 then
        self.relatedTitle:Hide()
        self.relatedRule:Hide()
        return startY
    end

    local y = startY
    self.relatedRule:ClearAllPoints()
    self.relatedRule:SetPoint("TOPLEFT", 0, -y)
    self.relatedRule:SetPoint("TOPRIGHT", parent, "TOPLEFT", width, -y)
    self.relatedRule:Show()
    y = y + 12

    self.relatedTitle:ClearAllPoints()
    self.relatedTitle:SetPoint("TOPLEFT", 0, -y)
    self.relatedTitle:SetText("You may also want to learn")
    self.relatedTitle:Show()
    y = y + self.relatedTitle:GetStringHeight() + 10

    local gap = 12
    local cols = math.max(2, math.min(n, math.floor((width + gap) / (150 + gap))))
    local tileW = math.floor((width - (cols - 1) * gap) / cols)
    local tileH = math.floor(tileW * TILE_ASPECT)

    for i = 1, n do
        local g = others[i]
        local t = self.relatedTiles[i]
        if not t then
            t = CreateTile(parent)
            self.relatedTiles[i] = t
        end
        local col = (i - 1) % cols
        local row = math.floor((i - 1) / cols)
        t:ClearAllPoints()
        t:SetPoint("TOPLEFT", col * (tileW + gap), -(y + row * (tileH + gap)))
        t:SetSize(tileW, tileH)
        LayoutTile(t, g, tileW, tileH)
        t:SetScript("OnClick", function() ns.GuideView:OpenGuide(g.slug) end)
    end

    local rows = math.ceil(n / cols)
    return y + rows * (tileH + gap)
end

function GuideView:OpenGuide(slug)
    BuildLookup()
    if not (ns.GuideBySlug and ns.GuideBySlug[slug]) then return end
    self.currentSlug = slug
    self.currentSection = "document"
    self.currentChapter = "overview"
    self.chapterScroll = {}

    local mf = ns.MainFrame
    local g = ns.GuideBySlug[slug]
    mf.currentSection = "blitz"

    -- Dropdown on the current battleground node: jump to any other battleground,
    -- mirroring the Adventure Guide's dungeon dropdown.
    local function siblingList()
        local list = {}
        for i, other in ipairs(ns.Guides or {}) do
            local s = other.slug
            list[#list + 1] = {
                text = other.name or s,
                id = i,
                func = function() ns.GuideView:OpenGuide(s) end,
            }
        end
        return list
    end

    mf:SetBreadcrumbTrail({
        { text = "Home", onClick = function() mf:GoHome() end },
        { text = "Battleground Blitz", onClick = function() mf:GoBlitz() end },
        { text = (g and g.name) or slug, list = siblingList },
    })

    self:RenderChapter("overview", false)
end

-- Battleground browser --------------------------------------------------------

function GuideView:HideList()
    if self.listButtons then
        for _, btn in ipairs(self.listButtons) do btn:Hide() end
    end
    if self.emptyText then self.emptyText:Hide() end
    self:HideGuideHeader()
    self:HideRelated()
    self:HideMapButton()
    if self.diffDropdown then self.diffDropdown:Hide() end
end

local function TileMatches(g, q)
    if not q or q == "" then return true end
    local hay = (g.name or ""):lower()
    local info = ns.GetMapInfo and ns.GetMapInfo(g.slug)
    if info and info.mapType then hay = hay .. " " .. info.mapType:lower() end
    return hay:find(q, 1, true) ~= nil
end

function GuideView:ShowList(filter)
    BuildLookup()
    local mf = ns.MainFrame
    mf:ClearContent()
    mf:SetSubnav(nil)
    mf:ShowSectionSidebar()
    mf:SetBreadcrumbTrail({
        { text = "Home", onClick = function() mf:GoHome() end },
        { text = "Battleground Blitz" },
    })
    self:HideGuideHeader()
    self:HideRelated()

    local parent = mf.content
    local width = mf:ContentWidth()
    local top = 4

    local q = filter and filter:lower():gsub("^%s+", ""):gsub("%s+$", "") or nil
    local list = {}
    for _, g in ipairs(ns.Guides or {}) do
        if TileMatches(g, q) then list[#list + 1] = g end
    end

    self.listButtons = self.listButtons or {}
    for _, b in ipairs(self.listButtons) do b:Hide() end

    if #list == 0 then
        if not self.emptyText then
            self.emptyText = parent:CreateFontString(nil, "OVERLAY", "GameFontDisable")
            self.emptyText:SetJustifyH("LEFT")
        end
        self.emptyText:ClearAllPoints()
        self.emptyText:SetPoint("TOPLEFT", 0, -top)
        self.emptyText:SetText("No battlegrounds match \"" .. (filter or "") .. "\".")
        self.emptyText:Show()
        parent:SetHeight(top + 40)
        mf:ScrollToTop()
        return
    end
    if self.emptyText then self.emptyText:Hide() end

    local gap = 12
    local cols = math.max(2, math.floor((width + gap) / (170 + gap)))
    local tileW = math.floor((width - (cols - 1) * gap) / cols)
    local tileH = math.floor(tileW * TILE_ASPECT)

    for i, g in ipairs(list) do
        local t = self.listButtons[i]
        if not t then
            t = CreateTile(parent)
            self.listButtons[i] = t
        end
        local col = (i - 1) % cols
        local row = math.floor((i - 1) / cols)
        t:ClearAllPoints()
        t:SetPoint("TOPLEFT", col * (tileW + gap), -(top + row * (tileH + gap)))
        t:SetSize(tileW, tileH)
        LayoutTile(t, g, tileW, tileH)
        t:SetScript("OnClick", function() ns.GuideView:OpenGuide(g.slug) end)
    end

    local rows = math.ceil(#list / cols)
    parent:SetHeight(math.max(top + rows * (tileH + gap) + 16, 10))
    mf:ScrollToTop()
end

BuildLookup()
