EbonholdHub.AdvisorView = {}

local UI = EbonholdHub.UI
local C  = UI.Colors

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

local HEADER_H   = 36
local TOOLBAR_H  = 28
local GAP        = 6
local ROW_H      = 34
local ICON_SIZE  = 24
local SCORE_W    = 40
local TIER_W     = 22
local PAD        = 10
local ECHO_TEXT_LEFT = PAD + ICON_SIZE + 6 + TIER_W + 6
local LEFT_W     = 310  -- fixed width of the stat-tier left panel

-- Chip constants (defined here so drag helpers below can reference them)
local CHIP_W      = 78
local CHIP_H      = 18
local CHIP_GAP    = 3
local TIER_LBL_W  = 22
local SECTION_PAD = 6

-- Tier colours (same as TierView)
local TIER_COLOR = {
    S = { 1.0,  0.82, 0.0  },
    A = { 0.63, 0.28, 0.94 },
    B = { 0.2,  0.5,  1.0  },
    C = { 0.45, 0.75, 0.45 },
    F = { 0.5,  0.2,  0.2  },
}

local QUALITY_RGB = {
    [0] = { 1.0,   1.0,   1.0   },
    [1] = { 0.12,  1.0,   0.0   },
    [2] = { 0.0,   0.44,  0.87  },
    [3] = { 0.64,  0.21,  0.93  },
    [4] = { 1.0,   0.5,   0.0   },
}

-- ── State ────────────────────────────────────────────────────────────────────
local panel
local classNameLabel, specNameLabel
local rowPool = {}

-- Floating echo-list window (like TierView's pool window)
local echoWindow = nil
local listScroll, listChild, emptyLabel

local currentClass  = nil
local currentSpec   = nil
local cachedResults = nil

-- Stat tier-list state (module-level so drag functions can access them)
local currentTiers      = {}  -- { statId = "S"|"A"|"B"|"C"|nil }
local currentStatOrder  = {}  -- { S={id,...}, A={...}, B, C, pool }
local statChips         = {}  -- { statId = chipFrame }
local statDropZones     = {}  -- { "S"=frame, "A"=frame, "B"=frame, "C"=frame, "pool"=frame }
local statRowFrames     = {}  -- { secId = frame }
local statRowHolders    = {}  -- { secId = chipHolder frame (inside row) }

-- Drag state
local statDragId         = nil
local statDragSourceTier = nil
local statDragGhost      = nil
local statDragFrame      = nil  -- persistent frame running OnUpdate

-- Forward declarations
local ApplyStatWeightsToAnalyzer
local RebuildStatLayout
local BuildColumnHeaders

-- ── Stat definitions: id matches SynergyData.preferredStats keys ─────────────
local STAT_LIST = {
    -- ── Primary stats ────────────────────────────────────────────────────────
    { id = "strength",      name = "Strength"    },
    { id = "agility",       name = "Agility"     },
    { id = "intellect",     name = "Intellect"   },
    { id = "stamina",       name = "Stamina"     },
    { id = "spirit",        name = "Spirit"      },
    -- ── Offensive secondary ──────────────────────────────────────────────────
    { id = "spellpower",    name = "Spell Power" },
    { id = "attackpower",   name = "Atk Power"   },
    { id = "crit",          name = "Crit"        },
    { id = "haste",         name = "Haste"       },
    { id = "hit",           name = "Hit"         },
    { id = "expertise",     name = "Expertise"   },
    { id = "arpen",         name = "Armor Pen"   },
    { id = "spellpen",      name = "Spell Pen"   },
    -- ── Damage / healing bonuses ─────────────────────────────────────────────
    { id = "damage_bonus",  name = "Dmg Bonus"   },
    { id = "healing_bonus", name = "Heal Bonus"  },
    { id = "absorb",        name = "Absorb"      },
    { id = "dot_damage",    name = "DoT Dmg"     },
    { id = "aoe_damage",    name = "AoE Dmg"     },
    { id = "cooldown",      name = "CDR"         },
    -- ── Resources ────────────────────────────────────────────────────────────
    { id = "mana",          name = "Mana"        },
    { id = "mp5",           name = "MP5"         },
    { id = "energy",        name = "Energy"      },
    { id = "rage",          name = "Rage"        },
    { id = "runicpower",    name = "Runic Power" },
    -- ── Pet / companion ──────────────────────────────────────────────────────
    { id = "pet",           name = "Pet Dmg"     },
    -- ── Proc / stack effects ─────────────────────────────────────────────────
    { id = "proc",          name = "Procs"       },
    { id = "stack",         name = "Stacks"      },
    -- ── Defensive secondary ──────────────────────────────────────────────────
    { id = "dodge",         name = "Dodge"       },
    { id = "parry",         name = "Parry"       },
    { id = "block_stat",    name = "Block"       },
    { id = "defense",       name = "Defense"     },
    { id = "armor",         name = "Armor"       },
    -- ── Utility ──────────────────────────────────────────────────────────────
    { id = "movespeed",     name = "Move Spd"    },
    -- ── PvP ──────────────────────────────────────────────────────────────────
    { id = "resilience",    name = "Resilience"  },
    -- ── Damage schools ───────────────────────────────────────────────────────
    { id = "physical",      name = "Physical"    },
    { id = "holy",          name = "Holy"        },
    { id = "fire",          name = "Fire"        },
    { id = "frost",         name = "Frost"       },
    { id = "nature",        name = "Nature"      },
    { id = "shadow",        name = "Shadow"      },
    { id = "arcane",        name = "Arcane"      },
    { id = "lightning",     name = "Lightning"   },
    -- ── Damage mechanics ─────────────────────────────────────────────────────
    { id = "bleed",         name = "Bleed"       },
    { id = "poison",        name = "Poison"      },
    { id = "disease",       name = "Disease"     },
}

-- ── Stat tier-list constants ───────────────────────────────────────────────
-- Tier → numeric weight passed to EchoAnalyzer (base; reduced by position within tier)
local STAT_TIER_WEIGHT = { S = 10, A = 7, B = 4, C = 1 }
-- Each slot to the right within the same tier shaves this much off the base weight.
local STAT_TIER_POSITION_STEP = 0.3
-- Click cycles: nil→S→A→B→C→nil
local STAT_TIER_NEXT = { S = "A", A = "B", B = "C" }   -- C→nil removes it
local STAT_TIER_COLOR = {
    S = { 1.0,  0.82, 0.0  },
    A = { 0.63, 0.28, 0.94 },
    B = { 0.2,  0.5,  1.0  },
    C = { 0.45, 0.75, 0.45 },
}
local STAT_TIER_SECTIONS = {
    { id = "S",    label = "S", color = STAT_TIER_COLOR.S },
    { id = "A",    label = "A", color = STAT_TIER_COLOR.A },
    { id = "B",    label = "B", color = STAT_TIER_COLOR.B },
    { id = "C",    label = "C", color = STAT_TIER_COLOR.C },
    { id = "pool", label = "-", color = { 0.38, 0.38, 0.38 } },
}
-- Default tier derived from spec profile weight thresholds
local function WeightToTier(w)
    if not w or w == 0 then return nil end
    if w >= 8 then return "S" end
    if w >= 5 then return "A" end
    if w >= 3 then return "B" end
    return "C"
end

-- ── Helpers ──────────────────────────────────────────────────────────────────

local function GetPlayerClass()
    local _, token = UnitClass("player")
    return token
end

local function GetSpecName(class, specIdx)
    local sd = EbonholdHub.SynergyData
    if sd and sd.SpecProfiles and sd.SpecProfiles[class] then
        local p = sd.SpecProfiles[class][specIdx]
        if p then return p.specName end
    end
    return "Spec " .. (specIdx or 1)
end

local CLASS_DISPLAY = {
    WARRIOR = "Warrior",  PALADIN = "Paladin",   HUNTER = "Hunter",
    ROGUE   = "Rogue",    PRIEST  = "Priest",    DEATHKNIGHT = "Death Knight",
    SHAMAN  = "Shaman",   MAGE    = "Mage",      WARLOCK = "Warlock",
    DRUID   = "Druid",
}

local function GetOverviewClassSpec()
    local BF = EbonholdHub.BuildForm
    if BF and BF.GetEditingClass then
        local cls = BF.GetEditingClass()
        if cls then
            local spec = BF.GetEditingSpec and BF.GetEditingSpec() or 1
            return cls, spec
        end
    end
    local build = EbonholdHub.Build and EbonholdHub.Build.GetActive()
    if build and build.class then
        return build.class, build.spec or 1
    end
    return GetPlayerClass(), 1
end

-- Returns true if this result was force-F'd because it belongs to another class.
local function IsClassRestricted(entry)
    if not entry.reasons then return false end
    for _, r in ipairs(entry.reasons) do
        if r:find("Class-restricted:", 1, true) then return true end
    end
    return false
end

local function GetFilteredResults()
    if not cachedResults then
        cachedResults = EbonholdHub.EchoAnalyzer.GetRecommendations(
            currentClass or GetPlayerClass(), 300, currentSpec)
    end
    -- Always return all tiers S→F, class-restricted echoes excluded.
    -- The list is scrollable; no filter button needed.
    local out = {}
    for _, r in ipairs(cachedResults) do
        if not IsClassRestricted(r) then
            out[#out + 1] = r
        end
    end
    return out
end

-- ── Row pool ─────────────────────────────────────────────────────────────────

local function AcquireRow(index, parent)
    local row = rowPool[index]
    if not row then
        row = CreateFrame("Frame", nil, parent)
        row:SetHeight(ROW_H)
        row._bg = SW.Fill(row, SC.elementBg)
        SW.ThinBorder(row, SC.border, 1)

        -- Left tier-colour accent strip
        local accent = row:CreateTexture(nil, "BORDER")
        accent:SetTexture(ST.FLAT)
        accent:SetVertexColor(unpack(SC.accent))
        accent:SetPoint("TOPLEFT",    row, "TOPLEFT",    0, 0)
        accent:SetPoint("BOTTOMLEFT", row, "BOTTOMLEFT", 0, 0)
        accent:SetWidth(2)
        row._accent = accent

        -- Icon
        local iconFrame = CreateFrame("Frame", nil, row)
        iconFrame:SetSize(ICON_SIZE, ICON_SIZE)
        iconFrame:SetPoint("LEFT", PAD, 0)
        SW.Fill(iconFrame, SC.bgElevated)

        local icon = iconFrame:CreateTexture(nil, "ARTWORK")
        icon:SetAllPoints(iconFrame)
        icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
        row._icon = icon

        -- Tier badge
        local tierBg = CreateFrame("Frame", nil, row)
        tierBg:SetSize(TIER_W, TIER_W)
        tierBg:SetPoint("LEFT", iconFrame, "RIGHT", 6, 0)
        local tierLabel = SW.Label(tierBg, "", 11, SC.text, false, "semibold")
        tierLabel:SetPoint("CENTER")
        row._tierBg    = tierBg
        row._tierBadge = tierLabel
        row._tierFill  = SW.Fill(tierBg, SC.bgElevated)

        -- Score badge (right side) — defined first so name/tags can anchor to it
        local scoreLabel = SW.Label(row, "", 10, SC.accent, false, "medium")
        scoreLabel:SetJustifyH("RIGHT")
        scoreLabel:SetPoint("RIGHT", -PAD, 0)
        scoreLabel:SetWidth(SCORE_W)
        row._scoreLabel = scoreLabel

        -- Name
        local nameLabel = SW.Label(row, "", 11, SC.text)
        nameLabel:SetJustifyH("LEFT")
        nameLabel:SetPoint("TOPLEFT",  tierBg,     "TOPRIGHT",  6, -2)
        nameLabel:SetPoint("TOPRIGHT", scoreLabel, "TOPLEFT",  -6, -2)
        row._nameLabel = nameLabel

        -- Tags line
        local tagLabel = SW.Label(row, "", 10, SC.textMuted)
        tagLabel:SetJustifyH("LEFT")
        tagLabel:SetPoint("BOTTOMLEFT",  tierBg,     "BOTTOMRIGHT", 6, 2)
        tagLabel:SetPoint("BOTTOMRIGHT", scoreLabel, "BOTTOMLEFT", -6, 2)
        row._tagLabel = tagLabel

        -- Hover highlight + tooltip
        row:EnableMouse(true)
        row:SetScript("OnEnter", function(self)
            self._bg:SetVertexColor(unpack(SC.bgElevated))
            if self._tooltipData then
                GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
                GameTooltip:ClearLines()
                local td   = self._tooltipData
                local name = GetSpellInfo(td.spellId)
                local headerLine = (name or td.name or "?")
                if td.qualLabel then
                    headerLine = headerLine .. " (" .. td.qualLabel .. ")"
                end
                GameTooltip:AddLine(headerLine, 1, 0.82, 0)
                if utils and utils.GetSpellDescription then
                    local desc = utils.GetSpellDescription(td.spellId, 500, 1)
                    if desc and desc ~= "" then
                        GameTooltip:AddLine(desc, 1, 1, 1, true)
                    end
                end
                if td.isConditional then
                    GameTooltip:AddLine("|cffFFCC44[Conditional vs Creature Type]|r", 1, 0.8, 0.27)
                    GameTooltip:AddLine("Bonus only active when fighting a specific enemy type.", 0.75, 0.65, 0.4, true)
                    GameTooltip:AddLine("Score reduced by 40% to reflect situational uptime.", 0.75, 0.65, 0.4, true)
                end
                if td.power then
                    GameTooltip:AddLine("Max value in description: " .. td.power, 0.6, 0.9, 0.6)
                end
                GameTooltip:AddLine(" ", 1, 1, 1)
                if td.tierEstimated then
                    GameTooltip:AddLine("Tier: estimated from quality (not set in Echoes tab)", 0.6, 0.6, 0.6, true)
                end
                if #td.reasons > 0 then
                    GameTooltip:AddLine("Why recommended:", 0.9, 0.75, 0.1)
                    for _, r in ipairs(td.reasons) do
                        GameTooltip:AddLine("  " .. r, 0.7, 0.85, 1.0, true)
                    end
                end
                GameTooltip:Show()
            end
        end)
        row:SetScript("OnLeave", function(self)
            self._bg:SetVertexColor(unpack(SC.elementBg))
            GameTooltip:Hide()
        end)

        rowPool[index] = row
    end
    row:SetParent(parent)
    row:Show()
    return row
end

local function HideUnusedRows(from)
    for i = from, #rowPool do
        if rowPool[i] then rowPool[i]:Hide() end
    end
end

-- ── Populate ─────────────────────────────────────────────────────────────────

local function UpdateEchoListWidth()
    if not listScroll or not listChild then return 0 end
    local w = listScroll.scrollFrame:GetWidth()
    if not w or w <= 0 then return 0 end
    w = math.max(1, math.floor(w))
    listChild:SetWidth(w)
    return w
end

local function Populate()
    if not listChild then return end

    local results = GetFilteredResults()

    if not results or #results == 0 then
        HideUnusedRows(1)
        if emptyLabel then
            emptyLabel:SetText("No echoes found for this class / filter.")
            emptyLabel:Show()
        end
        listChild:SetHeight(40)
        return
    end

    if emptyLabel then emptyLabel:Hide() end

    local listW = UpdateEchoListWidth()
    local totalH = 0
    for i, entry in ipairs(results) do
        local row = AcquireRow(i, listChild)
        local w = math.max(120, (listW > 0 and listW or listChild:GetWidth()) - 2)
        row:SetWidth(w)
        row:ClearAllPoints()
        row:SetPoint("TOPLEFT", listChild, "TOPLEFT", 1, -(totalH + 1))
        totalH = totalH + ROW_H + 2

        -- Icon
        local tex = entry.spellId and select(3, GetSpellInfo(entry.spellId))
        if tex then
            row._icon:SetTexture(tex)
            row._icon:SetVertexColor(1, 1, 1)
        else
            row._icon:SetTexture("Interface\\Icons\\INV_Misc_QuestionMark")
            row._icon:SetVertexColor(0.5, 0.5, 0.5)
        end

        -- F-tier rows are visually muted (crossed out is not possible, use heavy dim)
        local isFTier = (entry.tier == "F")
        local globalDim = isFTier and 0.45 or 1.0

        -- Tier badge (~prefix = estimated from quality, not manually set)
        local tc = TIER_COLOR[entry.tier] or TIER_COLOR["C"]
        row._tierFill:SetVertexColor(tc[1] * 0.25, tc[2] * 0.25, tc[3] * 0.25, 1)
        local tierText = entry.tierEstimated and ("~" .. (entry.tier or "?")) or (entry.tier or "?")
        row._tierBadge:SetText(tierText)
        local dimFactor = (entry.tierEstimated and 0.65 or 1.0) * globalDim
        row._tierBadge:SetTextColor(tc[1] * dimFactor, tc[2] * dimFactor, tc[3] * dimFactor)

        -- Name (quality-coloured) + quality label, truncated to avoid wrapping
        local qc = QUALITY_RGB[entry.quality] or QUALITY_RGB[0]
        row._nameLabel:SetTextColor(qc[1] * globalDim, qc[2] * globalDim, qc[3] * globalDim)
        local qualSuffix = entry.qualLabel and (" [" .. entry.qualLabel .. "]") or ""
        local nameStr = (entry.name or "") .. qualSuffix
        if #nameStr > 30 then nameStr = string.sub(nameStr, 1, 28) .. ".." end
        row._nameLabel:SetText(nameStr)

        -- Tags — skip the internal "conditional_vs_type" tag from the display list,
        -- and instead prefix the label with "(vs Type)" as a visual badge.
        local isConditional = false
        local tagStr = ""
        if entry.tags and #entry.tags > 0 then
            local shown = {}
            for _, t in ipairs(entry.tags) do
                if t == "conditional_vs_type" then
                    isConditional = true
                else
                    if #shown < 4 then shown[#shown + 1] = t end
                end
            end
            tagStr = table.concat(shown, ", ")
        end
        if isConditional then
            -- Amber badge to signal this echo is conditional vs a specific creature type
            row._tagLabel:SetText("|cffFFCC44[vs Type]|r " .. tagStr)
        else
            row._tagLabel:SetText(tagStr)
        end
        row._tagLabel:SetTextColor(SC.textMuted[1] * globalDim, SC.textMuted[2] * globalDim, SC.textMuted[3] * globalDim)

        -- Score
        row._scoreLabel:SetText(tostring(entry.score))
        row._scoreLabel:SetTextColor(SC.accent[1] * globalDim, SC.accent[2] * globalDim, SC.accent[3] * globalDim)

        -- Accent strip colour (tier), fully dimmed for F
        row._accent:SetVertexColor(tc[1] * globalDim, tc[2] * globalDim, tc[3] * globalDim)

        -- Tooltip data
        row._tooltipData = {
            spellId       = entry.spellId,
            name          = entry.name,
            qualLabel     = entry.qualLabel,
            reasons       = entry.reasons or {},
            tierEstimated = entry.tierEstimated,
            power         = entry.power,
            isConditional = isConditional,
        }
    end

    HideUnusedRows(#results + 1)
    listChild:SetHeight(math.max(40, totalH))
    if listScroll and listScroll.UpdateThumb then
        listScroll.UpdateThumb()
    end
end

-- ── Toolbar ──────────────────────────────────────────────────────────────────

-- Class/spec are read-only here; change them on the Overview tab.

-- BuildForcedFSet lives in SynergyData.BuildForcedFSet (shared with EchoAnalyzer).
-- Runs a full analysis (up to 600 echoes) regardless of display cache.
-- Uses ClassBlacklists to FORCE F-tier on class/role-inappropriate echoes,
-- then deduplicates by base name and writes S/A/B/F to TierData.
-- Within each tier: Epic (purple) always first; everything else by score.
local EPIC_QUALITY = 3

local function IsEpicEcho(r)
    return r and (r.quality or 0) == EPIC_QUALITY
end

local function CompareEchoApplyOrder(a, b)
    local epicA, epicB = IsEpicEcho(a), IsEpicEcho(b)
    if epicA ~= epicB then return epicA end
    local scoreA, scoreB = a.score or 0, b.score or 0
    if scoreA ~= scoreB then return scoreA > scoreB end
    local qualA, qualB = a.quality or 0, b.quality or 0
    if qualA ~= qualB then return qualA > qualB end
    return (a.name or "") < (b.name or "")
end

local function ApplyAdvisorTiers()
    local EA = EbonholdHub.EchoAnalyzer
    local TD = EbonholdHub.TierData
    if not EA or not TD then return end

    local cls = currentClass or GetPlayerClass()
    local spc = currentSpec or 1

    -- Full-coverage fetch: more than total echo count so nothing is missed
    local allResults = EA.GetRecommendations(cls, 600, spc)
    if not allResults or #allResults == 0 then
        DEFAULT_CHAT_FRAME:AddMessage("|cff88ccff[Advisor]|r No data available. Make sure you are in-game.")
        return
    end

    -- Build the AI-curated blacklist for this class/spec
    local forcedF = (SynergyData and SynergyData.BuildForcedFSet)
        and SynergyData.BuildForcedFSet(cls, spc) or {}

    -- Apply forced-F overrides before deduplication
    for _, r in ipairs(allResults) do
        if forcedF[r.name] then
            r.tier = "F"
            r.tierEstimated = false
        end
    end

    -- Group ALL quality variants into their scored tiers.
    -- Each quality of the same echo gets its own entry (Rare→B, Uncommon→B or C, etc.).
    -- C is the implicit "unassigned" default — skip it to avoid cluttering TierView.
    -- Forced-F echoes: one entry per name, keeping the highest-quality variant.
    local groups = { S = {}, A = {}, B = {}, F = {} }
    local fBest = {}  -- name → best forced-F result (highest quality)
    for _, r in ipairs(allResults) do
        if forcedF[r.name] then
            local prev = fBest[r.name]
            if not prev or CompareEchoApplyOrder(r, prev) then
                fBest[r.name] = r
            end
        else
            local g = groups[r.tier]
            if g then g[#g + 1] = r end
            -- C-tier echoes are intentionally skipped (unassigned = default)
        end
    end
    for _, r in pairs(fBest) do
        groups.F[#groups.F + 1] = r
    end

    -- Sort each group: Epic first, then score for the rest.
    for _, g in pairs(groups) do
        table.sort(g, CompareEchoApplyOrder)
    end

    -- Wipe current tiers and write S/A/B/F in sorted order.
    -- SetOrder after each tier so TierView left-to-right matches our sort.
    TD.ClearAll()
    local counts = { S = 0, A = 0, B = 0, F = 0 }
    for _, tier in ipairs({ "S", "A", "B", "F" }) do
        local orderKeys = {}
        for _, r in ipairs(groups[tier]) do
            TD.SetTier(r.name, tier, r.quality)
            orderKeys[#orderKeys + 1] = r.name .. "|" .. tostring(math.floor(tonumber(r.quality) or 0))
            counts[tier] = counts[tier] + 1
        end
        if TD.SetOrder then TD.SetOrder(tier, orderKeys) end
    end

    -- Sync display cache
    cachedResults = allResults

    if EbonholdHub.TierView then EbonholdHub.TierView.Refresh() end

    local cn = CLASS_DISPLAY[cls] or cls or "?"
    local sn = GetSpecName(cls, spc)
    DEFAULT_CHAT_FRAME:AddMessage(string.format(
        "|cff88ccff[Advisor]|r |cffffd700%s %s|r tiers applied: "
        .. "|cffFFD700S:%d|r  |cffA044EEA:%d|r  |cff3388FFB:%d|r  |cffaa3333F:%d|r"
        .. "  — Echoes tab: Epic (purple) first in each tier, then by score.",
        cn, sn, counts.S, counts.A, counts.B, counts.F))

    Populate()
end

-- BuildToolbar removed: all controls are now in BuildAdvisorHeader.

-- ── User stat weight helpers ──────────────────────────────────────────────────

-- Returns the current effective stat weights for the given class/spec.
-- Falls back to the spec profile defaults; user overrides take priority.
-- ── Stat tier storage (build-backed) ─────────────────────────────────────────

-- Returns { statId = "S"|"A"|"B"|"C"|nil } for the active build.
-- Falls back to spec defaults converted to tiers when the build has none.
local function BuildStatOrderFromTiers(tiers)
    local order = { S = {}, A = {}, B = {}, C = {}, pool = {} }
    for _, sd in ipairs(STAT_LIST) do
        local tier = tiers[sd.id] or "pool"
        local list = order[tier]
        if list then list[#list + 1] = sd.id end
    end
    return order
end

local function GetBuildStatTiers()
    local build = EbonholdHub.Build.GetActive()
    if build and EbonholdHub.Build.EnsureAdvisorStatTiers then
        EbonholdHub.Build.EnsureAdvisorStatTiers(build)
    end
    if build and build.advisorStatTiers and next(build.advisorStatTiers) then
        return build.advisorStatTiers
    end
    return {}
end

local function GetBuildStatTierOrder()
    local build = EbonholdHub.Build.GetActive()
    if build and build.advisorStatTierOrder and next(build.advisorStatTierOrder) then
        return build.advisorStatTierOrder
    end
    return BuildStatOrderFromTiers(GetBuildStatTiers())
end

local function SyncStatOrder(tiers, order)
    order = order or BuildStatOrderFromTiers(tiers)
    local synced = { S = {}, A = {}, B = {}, C = {}, pool = {} }
    local seen = {}
    for _, sec in ipairs({ "S", "A", "B", "C", "pool" }) do
        local src = order[sec] or {}
        for _, id in ipairs(src) do
            local belongs = (sec == "pool" and not tiers[id]) or (tiers[id] == sec)
            if belongs and not seen[id] then
                synced[sec][#synced[sec] + 1] = id
                seen[id] = true
            end
        end
    end
    for _, sd in ipairs(STAT_LIST) do
        if not seen[sd.id] then
            local sec = tiers[sd.id] or "pool"
            synced[sec][#synced[sec] + 1] = sd.id
            seen[sd.id] = true
        end
    end
    return synced
end

-- Converts tier + left-to-right order into per-stat weights for EchoAnalyzer.
-- Leftmost in a tier keeps the full tier base; each slot to the right is slightly weaker.
local function ComputeStatWeights(tiers, order)
    local weights = {}
    if not tiers or not next(tiers) then return weights end
    if not order or not next(order) then order = GetBuildStatTierOrder() end
    order = SyncStatOrder(tiers, order)
    for _, secId in ipairs({ "S", "A", "B", "C" }) do
        local base = STAT_TIER_WEIGHT[secId]
        local list = order[secId] or {}
        for pos, statId in ipairs(list) do
            if tiers[statId] == secId and base then
                local w = base - (pos - 1) * STAT_TIER_POSITION_STEP
                if w < 0.1 then w = 0.1 end
                weights[statId] = w
            end
        end
    end
    return weights
end

local function RemoveStatFromAllOrderLists(statId)
    for _, sec in ipairs(STAT_TIER_SECTIONS) do
        local list = currentStatOrder[sec.id]
        if list then
            for i, id in ipairs(list) do
                if id == statId then
                    table.remove(list, i)
                    break
                end
            end
        end
    end
end

local function StatTierInsertAt(statId, secId, pos)
    secId = secId or "pool"
    if not currentStatOrder or not next(currentStatOrder) then
        currentStatOrder = SyncStatOrder(currentTiers, GetBuildStatTierOrder())
    end

    RemoveStatFromAllOrderLists(statId)

    if secId == "pool" then
        currentTiers[statId] = nil
    else
        currentTiers[statId] = secId
    end

    local list = currentStatOrder[secId]
    if not list then list = {}; currentStatOrder[secId] = list end
    pos = math.max(1, math.min(pos or (#list + 1), #list + 1))
    table.insert(list, pos, statId)
end

local function SaveBuildStatTiers(tiers, order)
    local build = EbonholdHub.Build.GetActive()
    if build then
        build.advisorStatTiers = tiers
        if order then build.advisorStatTierOrder = order end
        build.advisorStatTiersCustomized = true
        build.advisorStatTiersFromDefaults = false
    end
    -- Mirror as numeric weights in SavedVariables for EchoAnalyzer compatibility
    local db = EbonholdHubDB
    if db and currentClass then
        db.statWeights = db.statWeights or {}
        db.statWeights[currentClass] = db.statWeights[currentClass] or {}
        db.statWeights[currentClass][currentSpec or 1] = ComputeStatWeights(tiers, order)
    end
end

local function ResetBuildStatTiers()
    local build = EbonholdHub.Build.GetActive()
    if build then
        build.advisorStatTiers = {}
        build.advisorStatTierOrder = {}
        build.advisorStatTiersCustomized = false
        if EbonholdHub.Build.ApplyDefaultAdvisorStatTiers then
            EbonholdHub.Build.ApplyDefaultAdvisorStatTiers(build)
        end
    end
    local db = EbonholdHubDB
    if db and db.statWeights and currentClass then
        if db.statWeights[currentClass] then
            db.statWeights[currentClass][currentSpec or 1] = nil
        end
    end
end

-- Apply tier + within-tier order → weight conversion to EchoAnalyzer.
ApplyStatWeightsToAnalyzer = function()
    local EA = EbonholdHub.EchoAnalyzer
    if not EA or not EA.SetStatOverrides then return end
    local tiers = GetBuildStatTiers()
    if not next(tiers) then
        EA.SetStatOverrides(nil)
        cachedResults = nil
        return
    end
    EA.SetStatOverrides(ComputeStatWeights(tiers, currentStatOrder))
    cachedResults = nil
end

local function SyncFromOverview()
    local cls, spec = GetOverviewClassSpec()
    local changed = (cls ~= currentClass) or (spec ~= currentSpec)
    currentClass = cls
    currentSpec  = spec
    if classNameLabel then
        classNameLabel:SetText(CLASS_DISPLAY[cls] or cls or "?")
    end
    if specNameLabel then
        specNameLabel:SetText(GetSpecName(cls, spec))
    end
    if changed then
        cachedResults = nil
        currentTiers = GetBuildStatTiers()
        currentStatOrder = SyncStatOrder(currentTiers, GetBuildStatTierOrder())
        ApplyStatWeightsToAnalyzer()
    end
    return changed
end

-- ── Stat drag-and-drop helpers ────────────────────────────────────────────────

local function DestroyStatDrag()
    statDragId = nil
    statDragSourceTier = nil
    if statDragGhost then statDragGhost:Hide(); statDragGhost = nil end
    if statDragFrame  then statDragFrame:SetScript("OnUpdate", nil) end
end

local function GetDropPositionInStatTier(tierId, cursorX, cursorY)
    local holder = statRowHolders[tierId]
    if not holder then return 1 end
    local ids = holder._orderedIds or {}
    local perRow = holder._perRow or 1
    local chipStep = CHIP_W + CHIP_GAP

    -- Exclude the chip being dragged (hidden during drag) for layout hit-test
    local visible = {}
    for _, id in ipairs(ids) do
        if id ~= statDragId then visible[#visible + 1] = id end
    end
    if #visible == 0 then return 1 end

    local holderTop = holder:GetTop()
    local holderLeft = holder:GetLeft()
    local targetRow = 0
    if holderTop and cursorY then
        local dy = holderTop - cursorY
        targetRow = math.max(0, math.floor(dy / chipStep))
    end

    for i, id in ipairs(visible) do
        local iconRow = math.floor((i - 1) / perRow)
        if iconRow == targetRow then
            local col = (i - 1) % perRow
            local cx = holderLeft and (holderLeft + col * chipStep)
            if cx and cursorX < (cx + CHIP_W / 2) then
                return i
            end
        elseif iconRow > targetRow then
            return i
        end
    end
    return #visible + 1
end

local function HitTestStatZone(x, y)
    for tierId, zone in pairs(statDropZones) do
        if zone:IsVisible() then
            local l = zone:GetLeft();  local r = zone:GetRight()
            local t = zone:GetTop();   local b = zone:GetBottom()
            if l and r and t and b and x >= l and x <= r and y >= b and y <= t then
                return tierId  -- "S","A","B","C", or "pool"
            end
        end
    end
    return "pool"  -- dropped outside → unassign
end

local function FinishStatDrop()
    if not statDragId then DestroyStatDrag(); return end
    local x, y = GetCursorPosition()
    local sc = UIParent:GetEffectiveScale()
    x, y = x / sc, y / sc
    local hit = HitTestStatZone(x, y)
    local pos = GetDropPositionInStatTier(hit, x, y)
    StatTierInsertAt(statDragId, hit, pos)
    SaveBuildStatTiers(currentTiers, currentStatOrder)
    ApplyStatWeightsToAnalyzer()
    RebuildStatLayout()
    Populate()
    DestroyStatDrag()
end

local function StartStatDrag(statId, statName, tierColor, sourceTier)
    statDragId = statId
    statDragSourceTier = sourceTier
    if statChips[statId] then statChips[statId]:Hide() end
    -- Ghost chip
    if statDragGhost then statDragGhost:Hide() end
    statDragGhost = CreateFrame("Frame", nil, UIParent)
    statDragGhost:SetSize(CHIP_W, CHIP_H)
    statDragGhost:SetFrameStrata("TOOLTIP")
    statDragGhost:SetAlpha(0.90)
    if tierColor then
        SW.Fill(statDragGhost, { tierColor[1]*0.18, tierColor[2]*0.18, tierColor[3]*0.18, 1 })
        SW.ThinBorder(statDragGhost, { tierColor[1]*0.55, tierColor[2]*0.55, tierColor[3]*0.55, 1 }, 1)
    else
        SW.Fill(statDragGhost, SC.bgElevated)
        SW.ThinBorder(statDragGhost, SC.border, 1)
    end
    local lbl = SW.Label(statDragGhost, statName, 10, SC.text)
    lbl:SetAllPoints(); lbl:SetJustifyH("CENTER")
    -- Immediately position at cursor
    local mx, my = GetCursorPosition(); local sc = UIParent:GetEffectiveScale()
    statDragGhost:SetPoint("CENTER", UIParent, "BOTTOMLEFT", mx/sc, my/sc)
    statDragGhost:Show()
    -- OnUpdate: follow cursor and detect mouse release
    if not statDragFrame then
        statDragFrame = CreateFrame("Frame", nil, UIParent)
    end
    statDragFrame:SetScript("OnUpdate", function()
        if not statDragGhost then return end
        if not IsMouseButtonDown("LeftButton") then
            FinishStatDrop()
            return
        end
        local nx, ny = GetCursorPosition(); local sc2 = UIParent:GetEffectiveScale()
        statDragGhost:ClearAllPoints()
        statDragGhost:SetPoint("CENTER", UIParent, "BOTTOMLEFT", nx/sc2, ny/sc2)
    end)
    statDragFrame:Show()
end

-- ── Stat tier-list panel ──────────────────────────────────────────────────────
-- Always-visible left panel.  Chips are dragged between tier rows.
-- Panel height is dynamic; chips reflow after every change.

-- TierView-style constants for stat rows
local STAT_ROW_LABEL_W = 24   -- matches TierView TIER_LABEL_W
local STAT_ROW_PAD     = 4    -- inner padding (matches TIER_PAD)
local STAT_ROW_MIN_H   = CHIP_H + STAT_ROW_PAD * 2

local function BuildStatTierPanel(parent)
    -- `parent` is the scroll child; rows go top-to-bottom.

    -- Build chip frames (module-level statChips, created once)
    local function MakeChip(statDef)
        local f = CreateFrame("Frame", nil, parent)
        f:SetSize(CHIP_W, CHIP_H)
        f._bg = SW.Fill(f, SC.elementBg)
        f._border = SW.ThinBorder(f, SC.border, 1)
        local lbl = SW.Label(f, statDef.name, 10, SC.textMuted)
        lbl:SetJustifyH("CENTER"); lbl:SetAllPoints()
        f._lbl = lbl
        f:EnableMouse(true)
        f:SetScript("OnMouseDown", function(self, btn)
            if btn == "LeftButton" then
                local tc = currentTiers[statDef.id] and STAT_TIER_COLOR[currentTiers[statDef.id]]
                local src = currentTiers[statDef.id] or "pool"
                StartStatDrag(statDef.id, statDef.name, tc, src)
            end
        end)
        f:SetScript("OnEnter", function(self)
            if statDragId then return end
            local t = currentTiers[statDef.id]
            GameTooltip:SetOwner(self, "ANCHOR_CURSOR")
            GameTooltip:SetText(statDef.name, 1, 1, 1)
            if t then
                local c = STAT_TIER_COLOR[t]
                local w = ComputeStatWeights(currentTiers, currentStatOrder)[statDef.id]
                GameTooltip:AddLine(string.format("Tier: %s  (weight %.1f)", t, w or 0), c[1], c[2], c[3])
            else
                GameTooltip:AddLine("Unassigned", 0.5, 0.5, 0.5)
            end
            GameTooltip:AddLine("Left = higher priority within the tier", 0.7, 0.7, 0.7)
            GameTooltip:Show()
        end)
        f:SetScript("OnLeave", function() GameTooltip:Hide() end)
        return f
    end
    for _, sd in ipairs(STAT_LIST) do
        statChips[sd.id] = MakeChip(sd)
    end

    -- Build tier rows (SW style: elementBg + border + colored label column)
    for _, sec in ipairs(STAT_TIER_SECTIONS) do
        local row = CreateFrame("Frame", nil, parent)
        row:SetPoint("TOPLEFT",  parent, "TOPLEFT",  0, 0)
        row:SetPoint("TOPRIGHT", parent, "TOPRIGHT", 0, 0)
        SW.Fill(row, SC.statCellBg)
        SW.ThinBorder(row, SC.border, 1)

        -- Colored label column — exact match of TierView BuildTierRow style
        local labelCol = CreateFrame("Frame", nil, row)
        labelCol:SetPoint("TOPLEFT",    row, "TOPLEFT",    0, 0)
        labelCol:SetPoint("BOTTOMLEFT", row, "BOTTOMLEFT", 0, 0)
        labelCol:SetWidth(STAT_ROW_LABEL_W)
        local lc = sec.color
        SW.Fill(labelCol, { lc[1], lc[2], lc[3], 0.12 })
        SW.ThinBorder(labelCol, { lc[1], lc[2], lc[3], 0.30 }, 1)

        local labelTxt = SW.Label(labelCol, sec.label, 14, sec.color, false, "semibold")
        labelTxt:SetPoint("CENTER", labelCol, "CENTER", 0, 0)

        -- Note for pool row
        if sec.id == "pool" then
            local note = SW.Label(row, "unassigned", 10, SC.textMuted)
            note:SetPoint("LEFT", labelCol, "RIGHT", STAT_ROW_PAD + 2, 0)
        end

        -- Chip holder (right of label column)
        local holder = CreateFrame("Frame", nil, row)
        holder:SetPoint("TOPLEFT",  labelCol, "TOPRIGHT",  STAT_ROW_PAD, -STAT_ROW_PAD)
        holder:SetPoint("TOPRIGHT", row,      "TOPRIGHT",  -STAT_ROW_PAD, -STAT_ROW_PAD)
        holder:SetHeight(CHIP_H)

        -- Register as drop zone
        statDropZones[sec.id] = row
        statRowFrames[sec.id]  = row
        statRowHolders[sec.id] = holder
    end

    -- ── RebuildStatLayout ─────────────────────────────────────────────────────
    RebuildStatLayout = function()
        local panelW = parent:GetWidth()
        if panelW <= 0 then panelW = 480 end
        local chipAreaW = panelW - STAT_ROW_LABEL_W - STAT_ROW_PAD * 2
        local perRow    = math.max(1, math.floor((chipAreaW + CHIP_GAP) / (CHIP_W + CHIP_GAP)))

        currentStatOrder = SyncStatOrder(currentTiers, currentStatOrder)

        -- Position rows top-to-bottom
        local yOffset = 0
        for _, sec in ipairs(STAT_TIER_SECTIONS) do
            local orderedIds = currentStatOrder[sec.id] or {}
            local lines   = math.max(1, math.ceil(#orderedIds / perRow))
            local rowH    = lines * (CHIP_H + CHIP_GAP) + STAT_ROW_PAD * 2

            local row    = statRowFrames[sec.id]
            local holder = statRowHolders[sec.id]
            row:ClearAllPoints()
            row:SetPoint("TOPLEFT",  parent, "TOPLEFT",  0, -yOffset)
            row:SetPoint("TOPRIGHT", parent, "TOPRIGHT", 0, -yOffset)
            row:SetHeight(rowH)

            holder:SetHeight(lines * (CHIP_H + CHIP_GAP))
            holder._orderedIds = orderedIds
            holder._perRow = perRow

            -- Place chips inside holder (left-to-right order within tier)
            for i, statId in ipairs(orderedIds) do
                local chip = statChips[statId]
                if chip then
                    chip:SetParent(holder)
                    chip:ClearAllPoints()
                    local idx  = i - 1
                    local col  = idx % perRow
                    local line = math.floor(idx / perRow)
                    chip:SetPoint("TOPLEFT", holder, "TOPLEFT",
                        col  * (CHIP_W + CHIP_GAP),
                        -line * (CHIP_H + CHIP_GAP))
                    chip:Show()
                    local t = currentTiers[statId]
                    if t then
                        local c = STAT_TIER_COLOR[t]
                        chip._bg:SetVertexColor(c[1]*0.15, c[2]*0.15, c[3]*0.15, 1)
                        SW.SetBorderColor(chip._border, { c[1]*0.45, c[2]*0.45, c[3]*0.45, 1 })
                        chip._lbl:SetTextColor(c[1], c[2], c[3])
                    else
                        chip._bg:SetVertexColor(unpack(SC.elementBg))
                        SW.SetBorderColor(chip._border, SC.border)
                        chip._lbl:SetTextColor(unpack(SC.textMuted))
                    end
                end
            end

            yOffset = yOffset + rowH + 1  -- +1 for border gap
        end

        -- Resize scroll child to fit all rows
        parent:SetHeight(yOffset + 2)
    end

    -- Rebuild when scroll child is sized
    parent:SetScript("OnSizeChanged", function(self, w)
        if w and w > 10 then RebuildStatLayout() end
    end)

    -- Initial load
    currentTiers = GetBuildStatTiers()
    currentStatOrder = SyncStatOrder(currentTiers, GetBuildStatTierOrder())
    RebuildStatLayout()
end

-- ── Floating echo-list window ────────────────────────────────────────────────

local function SaveEchoWindowPos()
    if not echoWindow then return end
    EbonholdHubDB = EbonholdHubDB or {}
    EbonholdHubDB.advisorEchoWindowPos = {
        x = echoWindow:GetLeft(),
        y = echoWindow:GetTop(),
    }
end

local function BuildEchoWindow()
    local ew = CreateFrame("Frame", "EbonholdHubAdvisorEchoWindow", UIParent)
    ew:SetSize(340, 500)
    ew:SetFrameStrata("DIALOG")
    ew:SetFrameLevel(100)
    ew:SetMovable(true)
    ew:EnableMouse(true)
    ew:RegisterForDrag("LeftButton")
    ew:SetClampedToScreen(true)
    ew:SetScript("OnDragStart", function(self) self:StartMoving() end)
    ew:SetScript("OnDragStop",  function(self)
        self:StopMovingOrSizing()
        SaveEchoWindowPos()
    end)

    SW.Fill(ew, SC.bg)
    SW.ThinBorder(ew, SC.border, 1)

    -- Left accent strip
    local accent = ew:CreateTexture(nil, "ARTWORK")
    accent:SetTexture(ST.FLAT)
    accent:SetVertexColor(unpack(SC.accent))
    accent:SetWidth(2)
    accent:SetPoint("TOPLEFT",    ew, "TOPLEFT",    0, 0)
    accent:SetPoint("BOTTOMLEFT", ew, "BOTTOMLEFT", 0, 0)

    -- Title bar
    local titleBar = CreateFrame("Frame", nil, ew)
    titleBar:SetHeight(30)
    titleBar:SetPoint("TOPLEFT",  ew, "TOPLEFT",  2, 0)
    titleBar:SetPoint("TOPRIGHT", ew, "TOPRIGHT", 0, 0)
    SW.Fill(titleBar, SC.sidebarBg)

    local titleLbl = SW.Label(titleBar, "Echo Recommendations", 12, SC.text, false, "semibold")
    titleLbl:SetPoint("LEFT", titleBar, "LEFT", 8, 0)

    -- Separator below title
    local sep = ew:CreateTexture(nil, "ARTWORK")
    sep:SetTexture(ST.FLAT)
    sep:SetVertexColor(unpack(SC.border))
    sep:SetHeight(1)
    sep:SetPoint("TOPLEFT",  titleBar, "BOTTOMLEFT",  0, 0)
    sep:SetPoint("TOPRIGHT", titleBar, "BOTTOMRIGHT", 0, 0)

    -- Column headers
    local colBar = BuildColumnHeaders(ew)
    colBar:ClearAllPoints()
    colBar:SetPoint("TOPLEFT",  sep, "BOTTOMLEFT",  2, -1)
    colBar:SetPoint("TOPRIGHT", sep, "BOTTOMRIGHT", 0, -1)
    colBar:SetHeight(18)

    -- Scrollable echo list
    listScroll = UI.CreateScrollFrame(ew)
    listScroll:SetPoint("TOPLEFT",     colBar, "BOTTOMLEFT",  0, -1)
    listScroll:SetPoint("BOTTOMRIGHT", ew,     "BOTTOMRIGHT", 0,  0)
    listChild = listScroll.scrollChild

    listScroll.scrollFrame:SetScript("OnSizeChanged", function()
        UpdateEchoListWidth()
        Populate()
    end)

    emptyLabel = SW.Label(listChild, "", 11, SC.textMuted)
    emptyLabel:SetPoint("TOPLEFT", 10, -10)
    emptyLabel:Hide()

    -- Restore saved position or place to the right of addon window
    local db = EbonholdHubDB
    if db and db.advisorEchoWindowPos then
        ew:SetPoint("TOPLEFT", UIParent, "BOTTOMLEFT",
            db.advisorEchoWindowPos.x,
            db.advisorEchoWindowPos.y)
    else
        ew:SetPoint("CENTER", UIParent, "CENTER", 350, 80)
    end

    ew:Hide()
    return ew
end

-- ── Header ────────────────────────────────────────────────────────────────────

local ADVISOR_HEADER_H = 64
local HEADER_PAD = 8

local function BuildAdvisorHeader(parent)
    local hp = CreateFrame("Frame", nil, parent)
    hp:SetPoint("TOPLEFT",  parent, "TOPLEFT",  0, 0)
    hp:SetPoint("TOPRIGHT", parent, "TOPRIGHT", 0, 0)
    hp:SetHeight(ADVISOR_HEADER_H)
    SW.Fill(hp, SC.sidebarBg)

    local sep = hp:CreateTexture(nil, "ARTWORK")
    sep:SetTexture(ST.FLAT)
    sep:SetVertexColor(unpack(SC.border))
    sep:SetHeight(1)
    sep:SetPoint("BOTTOMLEFT",  hp, "BOTTOMLEFT",  0, 0)
    sep:SetPoint("BOTTOMRIGHT", hp, "BOTTOMRIGHT", 0, 0)

    -- Right-side buttons (row 1)
    local refreshBtn = SW.CreateOutlineButton(hp, "Refresh", 72)
    refreshBtn:SetHeight(24)
    refreshBtn:SetPoint("TOPRIGHT", hp, "TOPRIGHT", -HEADER_PAD, -HEADER_PAD)
    refreshBtn:SetScript("OnClick", function()
        cachedResults = nil
        Populate()
    end)

    local applyBtn = SW.CreateAccentButton(hp, "Apply Tiers", function()
        ApplyAdvisorTiers()
    end)
    applyBtn:SetSize(96, 24)
    applyBtn:SetPoint("TOPRIGHT", refreshBtn, "TOPLEFT", -4, 0)
    applyBtn:SetScript("OnEnter", function(self)
        applyBtn._bg:SetVertexColor(unpack(SC.accentBg18))
        SW.SetBorderColor(applyBtn._border, SC.accentBorder55)
        GameTooltip:SetOwner(self, "ANCHOR_CURSOR")
        GameTooltip:ClearLines()
        GameTooltip:AddLine("Apply Advisor Tiers", 1, 0.82, 0)
        GameTooltip:AddLine("Fills the Echoes tab tier list with ranked", 1, 1, 1)
        GameTooltip:AddLine("recommendations for the build class/spec from Overview.", 1, 1, 1)
        GameTooltip:AddLine("|cffff6644Warning:|r Replaces your current tier assignments.", 1, 0.6, 0.4, true)
        GameTooltip:Show()
    end)
    applyBtn:SetScript("OnLeave", function()
        applyBtn._bg:SetVertexColor(unpack(SC.accentBg10))
        SW.SetBorderColor(applyBtn._border, SC.accentBorder35)
        GameTooltip:Hide()
    end)

    local resetBtn = SW.CreateOutlineButton(hp, "Reset", 60)
    resetBtn:SetHeight(24)
    resetBtn:SetPoint("TOPRIGHT", applyBtn, "TOPLEFT", -4, 0)
    resetBtn:SetScript("OnClick", function()
        ResetBuildStatTiers()
        currentTiers = GetBuildStatTiers()
        currentStatOrder = SyncStatOrder(currentTiers, GetBuildStatTierOrder())
        ApplyStatWeightsToAnalyzer()
        RebuildStatLayout()
        Populate()
    end)
    resetBtn:SetScript("OnEnter", function(self)
        SW.SetBorderColor(self._border, { 0.9, 0.35, 0.35, 1 })
        self._label:SetTextColor(0.95, 0.50, 0.50)
        GameTooltip:SetOwner(self, "ANCHOR_CURSOR")
        GameTooltip:SetText("Reset to spec defaults", 1, 1, 1)
        GameTooltip:Show()
    end)
    resetBtn:SetScript("OnLeave", function(self)
        SW.SetBorderColor(self._border, SC.border)
        self._label:SetTextColor(unpack(SC.textDim))
        GameTooltip:Hide()
    end)

    -- Row 1 left: class / spec labels — TOPLEFT so they sit on the same row as buttons
    -- Buttons: height=24 at y=-8 → center at y=-20 → label top ≈ -14 (for 13px label)
    local ROW1_LBL_Y = -(HEADER_PAD + 5)   -- ≈ -13, centers an 11pt label within 24px btn

    local classKeyLbl = SW.Label(hp, "Class:", 11, SC.textMuted)
    classKeyLbl:SetPoint("TOPLEFT", hp, "TOPLEFT", HEADER_PAD, ROW1_LBL_Y)

    local classValLbl = SW.Label(hp, CLASS_DISPLAY[currentClass] or currentClass or "?", 11, SC.text, false, "medium")
    classValLbl:SetPoint("LEFT", classKeyLbl, "RIGHT", 4, 0)
    classNameLabel = classValLbl

    local specKeyLbl = SW.Label(hp, "Spec:", 11, SC.textMuted)
    specKeyLbl:SetPoint("LEFT", classValLbl, "RIGHT", 12, 0)

    local specValLbl = SW.Label(hp, GetSpecName(currentClass, currentSpec or 1), 11, SC.text, false, "medium")
    specValLbl:SetPoint("LEFT", specKeyLbl, "RIGHT", 4, 0)
    specNameLabel = specValLbl

    -- Row 2: hint text — anchored to bottom half of header
    local hint = SW.Label(hp,
        "Drag stats into tiers below. Left = higher priority within the tier.",
        11, SC.textMuted)
    hint:SetPoint("TOPLEFT",  hp, "TOPLEFT",  HEADER_PAD, -(HEADER_PAD + 28))
    hint:SetPoint("TOPRIGHT", hp, "TOPRIGHT", -HEADER_PAD, 0)
    hint:SetJustifyH("LEFT")

    return hp
end

-- ── Column headers ───────────────────────────────────────────────────────────

BuildColumnHeaders = function(parent)
    local bar = CreateFrame("Frame", nil, parent)
    bar:SetHeight(18)
    SW.Fill(bar, SC.bgHeader)

    local lbl1 = SW.Label(bar, "Echo / Tags", 10, SC.textMuted)
    lbl1:SetPoint("LEFT", ECHO_TEXT_LEFT, 0)

    local lbl3 = SW.Label(bar, "Score", 10, SC.textMuted)
    lbl3:SetJustifyH("RIGHT")
    lbl3:SetPoint("RIGHT", -PAD, 0)

    return bar
end

-- ── Mount / Unmount ──────────────────────────────────────────────────────────

function EbonholdHub.AdvisorView.Mount(container)
    if not panel then
        panel = CreateFrame("Frame", nil, container)
        panel:SetAllPoints(container)

        -- TierView-style header panel (title + buttons + class/spec + hint)
        local hp = BuildAdvisorHeader(panel)

        -- Scrollable area for tier rows (like TierView's leftScroll)
        local leftScroll = CreateFrame("ScrollFrame", nil, panel)
        leftScroll:SetPoint("TOPLEFT",     hp,    "BOTTOMLEFT",  0, -2)
        leftScroll:SetPoint("BOTTOMRIGHT", panel, "BOTTOMRIGHT", 0,  0)
        leftScroll:EnableMouseWheel(true)

        local leftPanel = CreateFrame("Frame", nil, leftScroll)
        leftPanel:SetWidth(panel:GetWidth() or 500)
        leftPanel:SetHeight(32)
        leftScroll:SetScrollChild(leftPanel)

        leftScroll:SetScript("OnMouseWheel", function(self, delta)
            local childH = leftPanel:GetHeight() or 0
            local selfH  = self:GetHeight() or 0
            local max    = math.max(0, childH - selfH)
            if max <= 0 then return end
            local newPos = math.max(0, math.min(max,
                self:GetVerticalScroll() - delta * (CHIP_H + CHIP_GAP) * 2))
            self:SetVerticalScroll(newPos)
        end)
        leftScroll:SetScript("OnSizeChanged", function(self)
            local w = self:GetWidth()
            if w and w > 10 then
                leftPanel:SetWidth(math.floor(w))
                RebuildStatLayout()
            end
        end)

        BuildStatTierPanel(leftPanel)
    else
        panel:SetParent(container)
        panel:SetAllPoints(container)
    end

    -- Floating echo recommendations window
    if not echoWindow then
        echoWindow = BuildEchoWindow()
    end
    -- Always reset row pool so rows are recreated with current layout code
    for i = 1, #rowPool do
        if rowPool[i] then rowPool[i]:Hide(); rowPool[i]:SetParent(nil) end
    end
    rowPool = {}
    echoWindow:Show()

    SyncFromOverview()

    -- Apply any saved stat weight overrides for this class/spec
    ApplyStatWeightsToAnalyzer()

    cachedResults = nil
    panel:Show()
    Populate()
end

function EbonholdHub.AdvisorView.Unmount()
    if panel then panel:Hide() end
    if echoWindow then echoWindow:Hide() end
end

function EbonholdHub.AdvisorView.Init()
    local BF = EbonholdHub.BuildForm
    if BF and BF.OnMetaChanged then
        BF.OnMetaChanged(function()
            if panel and panel:IsShown() then
                if SyncFromOverview() then
                    RebuildStatLayout()
                    Populate()
                end
            end
        end)
    end
    if EbonholdHub.Build and EbonholdHub.Build.OnActiveChanged then
        EbonholdHub.Build.OnActiveChanged(function()
            if panel and panel:IsShown() then
                if SyncFromOverview() then
                    RebuildStatLayout()
                    Populate()
                end
            end
        end)
    end
end
