EbonholdHub.SessionHistory = {}

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

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

local TIER_COLOR = {
    S = ST.TIER_META[1].color,
    A = ST.TIER_META[2].color,
    B = ST.TIER_META[3].color,
    C = { 0.5, 0.5, 0.5 },
    F = ST.TIER_META[4].color,
}

local GAP = 8
local PAD = 14
local STATS_H = 44
local TOOLBAR_H = 30
local ENTRY_H    = 168
local ENTRY_GAP  = 10
local HEADER_H   = 22
local CHOICES_H  = 76
local FOOTER_H   = 22
local ICON_SIZE  = 30
local ICON_ZONE_H = 36
local SLOT_H     = CHOICES_H
local SLOT_GAP   = 8
local SLOT_PAD   = 6
local DIM_ICON     = { 0.38, 0.38, 0.44, 1.00 }
local CONTENT_INSET = 12
local DANGER = C_LEGACY.danger or { 0.95, 0.40, 0.40, 1 }

local ACTION_STYLE = {
    Banish = { color = DANGER, label = "Banished" },
    Reroll = { color = SC.accent, label = "Rerolled" },
    Freeze = { color = { 0.45, 0.78, 1.0, 1 }, label = "Frozen" },
    Select = { color = SC.success, label = "Chose echo" },
    ["Select (Locked)"] = { color = ST.TIER_META[1].color, label = "Locked echo" },
}

local statsBar, toolbar, logScroll, logScrollFrame, logChild, emptyLabel, clearBtn
local statLabels = {}
local logEntries = {}
local logViewVisible = false
local refreshDebounceFrame
local scrollWatchFrame
local lastScrollPos = -1
local cachedBestByName
local ROW_H = ENTRY_H + ENTRY_GAP

local function ActionStyle(action)
    if action == "Select (Locked)" then
        return ACTION_STYLE["Select (Locked)"]
    end
    local key = action and action:match("^(%a+)") or "Select"
    return ACTION_STYLE[key] or ACTION_STYLE.Select
end

local function TruncateName(name, maxLen)
    maxLen = maxLen or 11
    if not name then return "?" end
    if #name > maxLen then
        return name:sub(1, maxLen - 1) .. "…"
    end
    return name
end

local function GetActiveSession()
    return EbonholdHub.Session.GetActiveSession()
end

local function GetBestByNameCached()
    if not cachedBestByName and EbonholdHub.EchoTableRows and EbonholdHub.EchoTableRows.BuildBestByName then
        cachedBestByName = EbonholdHub.EchoTableRows.BuildBestByName()
    end
    return cachedBestByName
end

local function ResolveSpellId(ch, best)
    if not ch then return nil end
    if ch.spellId then return ch.spellId end
    if not ch.name then return nil end

    best = best or GetBestByNameCached()
    if best then
        local entry = best[ch.name]
        if entry then
            if ch.quality ~= nil and entry.spellIds and entry.spellIds[ch.quality] then
                return entry.spellIds[ch.quality]
            end
            return entry.spellId
        end
    end
    return nil
end

local function ApplyEchoIcon(iconBtn, ch, dimmed, best)
    local spellId = ResolveSpellId(ch, best)
    if spellId and iconBtn.icon then
        local _, _, tex = GetSpellInfo(spellId)
        if tex then iconBtn.icon:SetTexture(tex) end
        if dimmed then
            iconBtn.icon:SetVertexColor(unpack(DIM_ICON))
        else
            iconBtn.icon:SetVertexColor(1, 1, 1)
        end
    elseif iconBtn.icon then
        iconBtn.icon:SetTexture("Interface\\Icons\\INV_Misc_QuestionMark")
        iconBtn.icon:SetVertexColor(1, 1, 1)
    end
end

local function SetIconBtnQuality(iconBtn, quality, soft)
    if not iconBtn or not iconBtn._border then return end
    local qColor = ST.QUALITY_BORDER[quality] or ST.QUALITY_BORDER[2]
    if soft then
        qColor = ST.BlendRgb(qColor, SC.border, 0.55)
    end
    SW.SetBorderColor(iconBtn._border, qColor)
    if iconBtn._glow then
        iconBtn._glow:SetVertexColor(qColor[1], qColor[2], qColor[3], soft and 0.12 or 0.22)
    end
end

local function CreateLevelChip(parent)
    local chip = CreateFrame("Frame", nil, parent)
    chip:SetHeight(20)
    chip._bg = SW.Fill(chip, SC.elementBg)
    chip._border = SW.ThinBorder(chip, SC.border, 1)
    chip._prefix = SW.Label(chip, "LV", 8, SC.textMuted, false, "semibold")
    chip._prefix:SetPoint("LEFT", 7, 0)
    chip._value = SW.Label(chip, "?", 11, SC.accent, false, "semibold")
    chip._value:SetPoint("LEFT", chip._prefix, "RIGHT", 4, 0)
    chip.SetLevel = function(self, level)
        self._value:SetText(tostring(level or "?"))
        self:SetWidth(self._prefix:GetStringWidth() + self._value:GetStringWidth() + 15)
    end
    return chip
end

local function CreateActionChip(parent)
    local chip = CreateFrame("Frame", nil, parent)
    chip:SetHeight(20)
    chip._bg = SW.Fill(chip, SC.successBg10)
    chip._border = SW.ThinBorder(chip, SC.successBorder30, 1)
    chip._label = SW.Label(chip, "", 9, SC.success, false, "medium")
    chip._label:SetPoint("LEFT", 8, 0)
    chip._label:SetPoint("RIGHT", -8, 0)
    chip.SetAction = function(self, style)
        local color = style.color
        local bg = ST.BlendRgb(color, SC.bg, 0.10)
        local border = ST.BlendRgb(color, SC.border, 0.30)
        self._bg:SetVertexColor(unpack(bg))
        SW.SetBorderColor(self._border, border)
        self._label:SetText(style.label)
        self._label:SetTextColor(unpack(color))
        self:SetWidth(self._label:GetStringWidth() + 16)
    end
    return chip
end

local function CreateChargePill(parent, label, valueColor)
    local pill = CreateFrame("Frame", nil, parent)
    pill:SetHeight(FOOTER_H)
    pill._bg = SW.Fill(pill, SC.elementBg)
    pill._border = SW.ThinBorder(pill, SC.borderSoft, 1)
    pill._name = SW.Label(pill, string.upper(label or ""), 8, SC.textMuted, false, "semibold")
    pill._name:SetPoint("TOP", 0, -3)
    pill._value = SW.Label(pill, "0", 10, valueColor or SC.text, false, "semibold")
    pill._value:SetPoint("TOP", pill._name, "BOTTOM", 0, -1)
    pill.SetValue = function(self, n)
        self._value:SetText(tostring(n or 0))
    end
    return pill
end

local TIER_PICKER_META = {
    { id = "S", color = ST.TIER_META[1].color, desc = ST.TIER_META[1].desc },
    { id = "A", color = ST.TIER_META[2].color, desc = ST.TIER_META[2].desc },
    { id = "B", color = ST.TIER_META[3].color, desc = ST.TIER_META[3].desc },
    { id = "C", color = { 0.5, 0.5, 0.5 }, desc = "Neutral" },
    { id = "F", color = ST.TIER_META[4].color, desc = ST.TIER_META[4].desc },
}

local TIER_LABELS = {
    S = "Must have", A = "Good", B = "Decent", C = "Neutral", F = "Banned",
}

local PICKER_W = 256
local PICKER_H = 128
local PICKER_PAD = 10

local tierPickerFrame
local tierPickerCatcher

local function CloseTierPicker()
    GameTooltip:Hide()
    if tierPickerFrame then tierPickerFrame:Hide() end
    if tierPickerCatcher then tierPickerCatcher:Hide() end
end

local function HighlightTierButton(btn, active)
    if not btn or not btn._meta then return end
    local meta = btn._meta
    if active then
        btn._bg:SetVertexColor(unpack(ST.BlendRgb(meta.color, SC.bg, 0.32)))
        SW.SetBorderColor(btn._border, meta.color)
    else
        btn._bg:SetVertexColor(unpack(btn._defaultBg))
        SW.SetBorderColor(btn._border, ST.BlendRgb(meta.color, SC.border, 0.45))
    end
end

local function BuildTierPickerFrame()
    local f = CreateFrame("Frame", "EbonholdHubTierPicker", UIParent)
    f:SetSize(PICKER_W, PICKER_H)
    f:SetFrameStrata("TOOLTIP")
    f:EnableMouse(true)

    f._bg = SW.Fill(f, SC.bgElevated)
    f._border = SW.ThinBorder(f, SC.border, 1)

    local stripe = f:CreateTexture(nil, "ARTWORK")
    stripe:SetTexture(FLAT)
    stripe:SetVertexColor(unpack(SC.accent))
    stripe:SetWidth(3)
    stripe:SetPoint("TOPLEFT", f, "TOPLEFT", 0, 0)
    stripe:SetPoint("BOTTOMLEFT", f, "BOTTOMLEFT", 0, 0)

    local header = CreateFrame("Frame", nil, f)
    header:SetPoint("TOPLEFT", PICKER_PAD + 3, -PICKER_PAD)
    header:SetPoint("TOPRIGHT", -PICKER_PAD, -PICKER_PAD)
    header:SetHeight(20)

    SW.Label(header, "SET TIER", 9, SC.textMuted, false, "semibold"):SetPoint("LEFT", 0, 0)

    local closeBtn = CreateFrame("Button", nil, header)
    closeBtn:SetSize(18, 18)
    closeBtn:SetPoint("RIGHT", 0, 0)
    closeBtn._bg = SW.Fill(closeBtn, SC.elementBg)
    closeBtn._border = SW.ThinBorder(closeBtn, SC.borderSoft, 1)
    SW.Label(closeBtn, "×", 13, SC.textDim):SetPoint("CENTER", 0, 1)
    closeBtn:SetScript("OnEnter", function(self)
        SW.SetBorderColor(self._border, SC.borderHover)
        self._bg:SetVertexColor(unpack(SC.chipBg))
    end)
    closeBtn:SetScript("OnLeave", function(self)
        SW.SetBorderColor(self._border, SC.borderSoft)
        self._bg:SetVertexColor(unpack(SC.elementBg))
    end)
    closeBtn:SetScript("OnClick", CloseTierPicker)

    local headerSep = f:CreateTexture(nil, "ARTWORK")
    headerSep:SetTexture(FLAT)
    headerSep:SetVertexColor(unpack(SC.borderSoft))
    headerSep:SetHeight(1)
    headerSep:SetPoint("TOPLEFT", header, "BOTTOMLEFT", 0, -5)
    headerSep:SetPoint("TOPRIGHT", header, "BOTTOMRIGHT", 0, -5)

    local echoRow = CreateFrame("Frame", nil, f)
    echoRow:SetPoint("TOPLEFT", headerSep, "BOTTOMLEFT", 0, -7)
    echoRow:SetPoint("TOPRIGHT", headerSep, "BOTTOMRIGHT", 0, -7)
    echoRow:SetHeight(36)

    f._echoIcon = SW.CreateEchoIcon(echoRow, {
        size = 30,
        showBadge = false,
        showGlow = false,
        softBorder = true,
    })
    f._echoIcon:SetPoint("LEFT", 0, 0)
    f._echoIcon:EnableMouse(false)

    f._echoNameLabel = SW.Label(echoRow, "", 11, SC.text, false, "semibold")
    f._echoNameLabel:SetPoint("TOPLEFT", f._echoIcon, "TOPRIGHT", 8, -2)
    f._echoNameLabel:SetPoint("RIGHT", echoRow, "RIGHT", -28, -2)
    f._echoNameLabel:SetJustifyH("LEFT")
    f._echoNameLabel:SetWordWrap(false)

    f._echoHint = SW.Label(echoRow, "Click a tier below", 9, SC.textMuted)
    f._echoHint:SetPoint("TOPLEFT", f._echoNameLabel, "BOTTOMLEFT", 0, -2)

    f._currentTier = SW.Label(echoRow, "", 12, SC.accent, true, "semibold")
    f._currentTier:SetPoint("RIGHT", echoRow, "RIGHT", 0, 0)

    local tierRow = CreateFrame("Frame", nil, f)
    tierRow:SetPoint("BOTTOMLEFT", f, "BOTTOMLEFT", PICKER_PAD + 3, PICKER_PAD)
    tierRow:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -PICKER_PAD, PICKER_PAD)
    tierRow:SetHeight(34)

    f._tierBtns = {}
    local btnSize = 32
    local btnGap = 6
    local totalBtnW = #TIER_PICKER_META * btnSize + (#TIER_PICKER_META - 1) * btnGap
    local startX = math.max(0, math.floor((PICKER_W - PICKER_PAD * 2 - 3 - totalBtnW) / 2))

    for i, meta in ipairs(TIER_PICKER_META) do
        local btn = CreateFrame("Button", nil, tierRow)
        btn:SetSize(btnSize, btnSize)
        btn:SetPoint("TOPLEFT", startX + (i - 1) * (btnSize + btnGap), 0)
        btn._meta = meta
        btn._defaultBg = ST.BlendRgb(meta.color, SC.bg, 0.14)
        btn._bg = SW.Fill(btn, btn._defaultBg)
        btn._border = SW.ThinBorder(btn, ST.BlendRgb(meta.color, SC.border, 0.45), 1)
        SW.Label(btn, meta.id, 12, meta.color, true, "semibold"):SetPoint("CENTER")

        btn:SetScript("OnEnter", function(self)
            HighlightTierButton(self, true)
            local desc = TIER_LABELS[meta.id] or meta.desc or ""
            f._echoHint:SetText(meta.id .. " — " .. desc)
            f._echoHint:SetTextColor(meta.color[1], meta.color[2], meta.color[3])
        end)
        btn:SetScript("OnLeave", function(self)
            local current = f._currentTierId
            HighlightTierButton(self, current == meta.id)
            f._echoHint:SetText("Click a tier below")
            f._echoHint:SetTextColor(unpack(SC.textMuted))
        end)
        btn:SetScript("OnClick", function()
            if f._echoName and EbonholdHub.TierData then
                EbonholdHub.TierData.SetTier(f._echoName, meta.id)
                CloseTierPicker()
                EbonholdHub.SessionHistory.RefreshLogView()
                if EbonholdHub.TierView and EbonholdHub.TierView.Refresh then
                    EbonholdHub.TierView.Refresh()
                end
            end
        end)

        f._tierBtns[meta.id] = btn
    end

    f.SetEcho = function(self, echoName, spellId, currentTier)
        self._echoName = echoName
        self._currentTierId = currentTier

        local displayName = echoName or "?"
        if spellId then
            displayName = GetSpellInfo(spellId) or displayName
        end
        if #displayName > 22 then
            displayName = displayName:sub(1, 21) .. "…"
        end
        self._echoNameLabel:SetText(displayName)

        if self._echoIcon and self._echoIcon.icon then
            if spellId then
                local _, _, tex = GetSpellInfo(spellId)
                if tex then self._echoIcon.icon:SetTexture(tex) end
                self._echoIcon.icon:SetVertexColor(1, 1, 1)
            else
                self._echoIcon.icon:SetTexture("Interface\\Icons\\INV_Misc_QuestionMark")
            end
        end

        if currentTier then
            local tc = TIER_COLOR[currentTier] or SC.textMuted
            self._currentTier:SetText(currentTier)
            self._currentTier:SetTextColor(unpack(tc))
        else
            self._currentTier:SetText("—")
            self._currentTier:SetTextColor(unpack(SC.textMuted))
        end

        for id, btn in pairs(self._tierBtns) do
            HighlightTierButton(btn, id == currentTier)
        end
    end

    return f
end

local function ShowTierPicker(anchor, echoName, spellId, currentTier)
    if not EbonholdHub.TierData then return end
    GameTooltip:Hide()

    if not tierPickerCatcher then
        local catcher = CreateFrame("Frame", nil, UIParent)
        catcher:SetAllPoints(UIParent)
        catcher:SetFrameStrata("FULLSCREEN")
        catcher:EnableMouse(true)
        catcher:SetScript("OnMouseDown", CloseTierPicker)
        catcher:Hide()
        tierPickerCatcher = catcher
    end

    if not tierPickerFrame then
        tierPickerFrame = BuildTierPickerFrame()
    end

    spellId = spellId or (anchor and anchor._spellId)
    currentTier = currentTier or (anchor and anchor._echoTier)
    tierPickerFrame:SetEcho(echoName, spellId, currentTier)

    tierPickerFrame:ClearAllPoints()
    tierPickerFrame:SetPoint("BOTTOM", anchor, "TOP", 0, 8)
    tierPickerFrame:SetFrameLevel((anchor:GetFrameLevel() or 1) + 30)
    tierPickerFrame:Show()
    tierPickerCatcher:SetFrameLevel(tierPickerFrame:GetFrameLevel() - 1)
    tierPickerCatcher:Show()
end

local function WireIconButton(iconBtn)
    iconBtn:SetScript("OnEnter", function(self)
        if self._bg then
            self._bg:SetVertexColor(unpack(SC.elementBg))
        end
        SW.SetBorderColor(self._border, SC.accentBorder55)
        if not self._echoName then return end
        GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
        if GameTooltip.SetFrameStrata then
            GameTooltip:SetFrameStrata("TOOLTIP")
        end
        if GameTooltip.SetFrameLevel then
            GameTooltip:SetFrameLevel((self:GetFrameLevel() or 1) + 20)
        end
        GameTooltip:ClearLines()
        local displayName = self._echoName
        if self._spellId then
            displayName = GetSpellInfo(self._spellId) or self._echoName
        end
        GameTooltip:AddLine(displayName, 1, 0.82, 0)
        if self._spellId and utils and utils.GetSpellDescription then
            local desc = utils.GetSpellDescription(self._spellId, 500, 1)
            if desc and desc ~= "" then
                GameTooltip:AddLine(desc, 1, 1, 1, true)
            end
        end
        if self._echoTier then
            local tc = TIER_COLOR[self._echoTier]
            if tc then
                GameTooltip:AddLine("Tier: " .. self._echoTier, tc[1], tc[2], tc[3])
            end
        end
        GameTooltip:AddLine("Click to change tier", SC.textMuted[1], SC.textMuted[2], SC.textMuted[3])
        GameTooltip:Show()
    end)
    iconBtn:SetScript("OnLeave", function(self)
        GameTooltip:Hide()
        if self._restoreStyle then
            self._restoreStyle()
        end
    end)
    iconBtn:SetScript("OnClick", function(self)
        if self._echoName then
            ShowTierPicker(self, self._echoName, self._spellId, self._echoTier)
        end
    end)
    iconBtn:EnableMouse(true)
    iconBtn:RegisterForClicks("LeftButtonUp")
end

local function ApplySlotCardStyle(slot, selected)
    if not slot.card then return end
    if selected then
        slot.card._bg:SetVertexColor(unpack(SC.accentBg10))
        SW.SetBorderColor(slot.card._border, SC.accentBorder35)
    else
        slot.card._bg:SetVertexColor(unpack(SC.chipBg))
        SW.SetBorderColor(slot.card._border, SC.borderSoft)
    end
end

local function CreateChoiceSlot(parent)
    local slot = CreateFrame("Frame", nil, parent)

    local card = CreateFrame("Frame", nil, slot)
    card:SetAllPoints(slot)
    card._bg = SW.Fill(card, SC.chipBg)
    card._border = SW.ThinBorder(card, SC.borderSoft, 1)
    slot.card = card

    local iconZone = CreateFrame("Frame", nil, card)
    iconZone:SetPoint("TOPLEFT", SLOT_PAD, -SLOT_PAD)
    iconZone:SetPoint("TOPRIGHT", -SLOT_PAD, -SLOT_PAD)
    iconZone:SetHeight(ICON_ZONE_H)
    slot.iconZone = iconZone

    local iconBtn = SW.CreateEchoIcon(iconZone, {
        size = ICON_SIZE,
        showBadge = false,
        showGlow = false,
        borderThickness = 1,
        softBorder = true,
    })
    iconBtn:SetPoint("CENTER", iconZone, "CENTER", 0, 0)
    slot.iconBtn = iconBtn
    WireIconButton(iconBtn)

    local textZone = CreateFrame("Frame", nil, card)
    textZone:SetPoint("TOPLEFT", iconZone, "BOTTOMLEFT", 0, -4)
    textZone:SetPoint("BOTTOMRIGHT", card, "BOTTOMRIGHT", -SLOT_PAD, SLOT_PAD)
    slot.textZone = textZone

    local nameLabel = SW.Label(textZone, "", 9, SC.textMuted)
    nameLabel:SetPoint("TOPLEFT", 0, 0)
    nameLabel:SetPoint("TOPRIGHT", 0, 0)
    nameLabel:SetHeight(12)
    nameLabel:SetJustifyH("CENTER")
    nameLabel:SetWordWrap(false)
    slot.nameLabel = nameLabel

    local scoreLabel = SW.Label(textZone, "", 8, SC.textMuted)
    scoreLabel:SetPoint("TOPLEFT", nameLabel, "BOTTOMLEFT", 0, -2)
    scoreLabel:SetPoint("TOPRIGHT", nameLabel, "BOTTOMRIGHT", 0, -2)
    scoreLabel:SetHeight(10)
    scoreLabel:SetJustifyH("CENTER")
    slot.scoreLabel = scoreLabel

    return slot
end

local function PopulateEntrySlots(entryFrame, entry, slotW, best)
    local targetIndex = entry.targetIndex
    local totalW = slotW * 3 + SLOT_GAP * 2
    local areaW = entryFrame.choicesArea:GetWidth() or totalW
    local startX = math.max(0, math.floor((areaW - totalW) / 2))

    for i = 1, 3 do
        local slot = entryFrame.slots[i]
        local ch = entry.choices[i]
        local isTarget = (i == targetIndex)

        slot:ClearAllPoints()
        slot:SetPoint("TOP", entryFrame.choicesArea, "TOP", 0, 0)
        slot:SetPoint("LEFT", entryFrame.choicesArea, "LEFT", startX + (i - 1) * (slotW + SLOT_GAP), 0)
        slot:SetSize(slotW, entryFrame.choicesArea:GetHeight() or CHOICES_H)

        local liveTier
        if ch then
            liveTier = (ch.name and EbonholdHub.TierData)
                and EbonholdHub.TierData.GetTier(ch.name, ch.quality)
                or ch.tier
        end

        local function RestoreSlotStyle()
            ApplySlotCardStyle(slot, ch and isTarget)
            if ch and isTarget then
                if slot.iconBtn._bg then
                    slot.iconBtn._bg:SetVertexColor(unpack(SC.elementBg))
                end
                SetIconBtnQuality(slot.iconBtn, ch.quality, false)
            elseif ch then
                if slot.iconBtn._bg then
                    slot.iconBtn._bg:SetVertexColor(unpack(SC.elementBg))
                end
                SetIconBtnQuality(slot.iconBtn, ch.quality, true)
            end
        end

        if ch then
            ApplyEchoIcon(slot.iconBtn, ch, not isTarget, best)
            slot.nameLabel:SetText(TruncateName(ch.name, 12))
            local tierCh = {}
            for k, v in pairs(ch) do tierCh[k] = v end
            tierCh.tier = liveTier
            tierCh.isTierF = (liveTier == "F")
            slot.scoreLabel:SetText(EbonholdHub.Scoring.FormatChoiceScore(tierCh))
            slot.iconBtn._echoName = ch.name
            slot.iconBtn._echoTier = liveTier
            slot.iconBtn._spellId = ResolveSpellId(ch, best)
        else
            if slot.iconBtn.icon then slot.iconBtn.icon:SetTexture(nil) end
            slot.nameLabel:SetText("")
            slot.scoreLabel:SetText("")
            slot.iconBtn._echoName = nil
            slot.iconBtn._echoTier = nil
            slot.iconBtn._spellId = nil
        end

        slot.iconBtn._restoreStyle = RestoreSlotStyle

        if ch and isTarget then
            ApplySlotCardStyle(slot, true)
            SetIconBtnQuality(slot.iconBtn, ch.quality, false)
            slot.nameLabel:SetTextColor(unpack(SC.text))
            local tc = TIER_COLOR[liveTier]
            if tc then
                slot.scoreLabel:SetTextColor(unpack(tc))
            else
                slot.scoreLabel:SetTextColor(unpack(SC.textDim))
            end
        else
            ApplySlotCardStyle(slot, false)
            SetIconBtnQuality(slot.iconBtn, ch and ch.quality, true)
            slot.nameLabel:SetTextColor(unpack(SC.textMuted))
            local tc = TIER_COLOR[liveTier]
            if tc then
                slot.scoreLabel:SetTextColor(tc[1] * 0.55, tc[2] * 0.55, tc[3] * 0.55)
            else
                slot.scoreLabel:SetTextColor(unpack(SC.textMuted))
            end
        end
        slot:Show()
    end
end

local function LayoutChargePills(row, pills, gap)
    if not row or not pills or #pills == 0 then return end
    local w = row:GetWidth()
    if not w or w <= 0 then return end
    gap = gap or 6
    local pillW = math.floor((w - gap * (#pills - 1)) / #pills)
    for i, pill in ipairs(pills) do
        pill:SetWidth(pillW)
        pill:ClearAllPoints()
        pill:SetPoint("TOP", row, "TOP", 0, 0)
        pill:SetPoint("LEFT", row, "LEFT", (i - 1) * (pillW + gap), 0)
    end
end

local function BuildLogEntry(parent)
    local entry = CreateFrame("Frame", nil, parent)
    entry:SetHeight(ENTRY_H)
    SW.Fill(entry, SC.statCellBg)
    entry._border = SW.ThinBorder(entry, SC.border, 1)

    entry.accentStripe = entry:CreateTexture(nil, "ARTWORK")
    entry.accentStripe:SetTexture(FLAT)
    entry.accentStripe:SetVertexColor(unpack(SC.accent))
    entry.accentStripe:SetWidth(3)
    entry.accentStripe:SetPoint("TOPLEFT", entry, "TOPLEFT", 0, 0)
    entry.accentStripe:SetPoint("BOTTOMLEFT", entry, "BOTTOMLEFT", 0, 0)

    entry.headerRow = CreateFrame("Frame", nil, entry)
    entry.headerRow:SetPoint("TOPLEFT", entry, "TOPLEFT", PAD + 4, -PAD)
    entry.headerRow:SetPoint("TOPRIGHT", entry, "TOPRIGHT", -PAD, -PAD)
    entry.headerRow:SetHeight(HEADER_H)

    entry.levelChip = CreateLevelChip(entry.headerRow)
    entry.levelChip:SetPoint("LEFT", entry.headerRow, "LEFT", 0, 0)

    entry.actionChip = CreateActionChip(entry.headerRow)
    entry.actionChip:SetPoint("LEFT", entry.levelChip, "RIGHT", 8, 0)

    entry.headerSep = entry:CreateTexture(nil, "ARTWORK")
    entry.headerSep:SetTexture(FLAT)
    entry.headerSep:SetVertexColor(unpack(SC.borderSoft))
    entry.headerSep:SetHeight(1)
    entry.headerSep:SetPoint("TOPLEFT", entry.headerRow, "BOTTOMLEFT", 0, -5)
    entry.headerSep:SetPoint("TOPRIGHT", entry.headerRow, "BOTTOMRIGHT", 0, -5)

    entry.footerSep = entry:CreateTexture(nil, "ARTWORK")
    entry.footerSep:SetTexture(FLAT)
    entry.footerSep:SetVertexColor(unpack(SC.borderSoft))
    entry.footerSep:SetHeight(1)
    entry.footerSep:SetPoint("BOTTOMLEFT", entry, "BOTTOMLEFT", PAD, PAD + FOOTER_H + 5)
    entry.footerSep:SetPoint("BOTTOMRIGHT", entry, "BOTTOMRIGHT", -PAD, PAD + FOOTER_H + 5)

    entry.chargesRow = CreateFrame("Frame", nil, entry)
    entry.chargesRow:SetPoint("BOTTOMLEFT", entry, "BOTTOMLEFT", PAD, PAD)
    entry.chargesRow:SetPoint("BOTTOMRIGHT", entry, "BOTTOMRIGHT", -PAD, PAD)
    entry.chargesRow:SetHeight(FOOTER_H)

    entry.banPill = CreateChargePill(entry.chargesRow, "Ban", DANGER)
    entry.rerollPill = CreateChargePill(entry.chargesRow, "Reroll", SC.accent)
    entry.freezePill = CreateChargePill(entry.chargesRow, "Freeze", { 0.55, 0.75, 0.95, 1 })
    entry._chargePills = { entry.banPill, entry.rerollPill, entry.freezePill }

    entry.choicesArea = CreateFrame("Frame", nil, entry)
    entry.choicesArea:SetPoint("TOPLEFT", entry.headerSep, "BOTTOMLEFT", 0, -7)
    entry.choicesArea:SetPoint("BOTTOMRIGHT", entry.footerSep, "TOPRIGHT", 0, 7)

    entry.slots = {}
    for i = 1, 3 do
        entry.slots[i] = CreateChoiceSlot(entry.choicesArea)
    end

    entry:Hide()
    return entry
end

local function EnsureLogEntry(poolIndex)
    if not logEntries[poolIndex] then
        logEntries[poolIndex] = BuildLogEntry(logChild)
    end
    return logEntries[poolIndex]
end

local function HideUnusedLogEntries(usedCount)
    for i = usedCount + 1, #logEntries do
        logEntries[i]:Hide()
    end
end

local function PopulateLogCard(card, entry, slotW, best)
    card.levelChip:SetLevel(entry.level)
    local style = ActionStyle(entry.action)
    card.actionChip:SetAction(style)
    LayoutChargePills(card.chargesRow, card._chargePills, 6)
    PopulateEntrySlots(card, entry, slotW, best)
    local ch = entry.charges or {}
    card.banPill:SetValue(ch.ban or 0)
    card.rerollPill:SetValue(ch.reroll or 0)
    card.freezePill:SetValue(ch.freeze or 0)
end

local function LayoutStatsBar(bar)
    if not bar or not bar._cells then return end
    local w = bar:GetWidth()
    if not w or w <= 12 then return end
    local gap = 6
    local cellW = math.floor((w - gap) / 2)
    for i, cell in ipairs(bar._cells) do
        cell:SetWidth(cellW)
        cell:ClearAllPoints()
        cell:SetPoint("TOPLEFT", bar, "TOPLEFT", (i - 1) * (cellW + gap), 0)
    end
end

local function RefreshStatsBar(session, logs)
    if not statLabels then return end
    local count = logs and #logs or 0
    if statLabels.picks then
        statLabels.picks:SetText(tostring(count))
        statLabels.picks:SetTextColor(unpack(count > 0 and SC.text or SC.textMuted))
    end
    if statLabels.last then
        if count > 0 then
            local last = logs[count]
            local style = ActionStyle(last.action)
            statLabels.last:SetText(style.label)
            statLabels.last:SetTextColor(unpack(style.color))
        elseif session then
            statLabels.last:SetText("Waiting")
            statLabels.last:SetTextColor(unpack(SC.textMuted))
        else
            statLabels.last:SetText("No run")
            statLabels.last:SetTextColor(unpack(SC.textMuted))
        end
    end
end

function EbonholdHub.SessionHistory.StopWork()
    if refreshDebounceFrame then refreshDebounceFrame:Hide() end
    if scrollWatchFrame then scrollWatchFrame:Hide() end
    CloseTierPicker()
end

function EbonholdHub.SessionHistory.RefreshLogViewIfVisible()
    if not logViewVisible then return end
    if not refreshDebounceFrame then
        refreshDebounceFrame = CreateFrame("Frame")
        refreshDebounceFrame:Hide()
        refreshDebounceFrame:SetScript("OnUpdate", function(self, dt)
            if not logViewVisible then
                self:Hide()
                return
            end
            self._elapsed = (self._elapsed or 0) + dt
            if self._elapsed < 0.35 then return end
            self._elapsed = 0
            self:Hide()
            EbonholdHub.SessionHistory.RefreshLogView()
        end)
    end
    refreshDebounceFrame._elapsed = 0
    refreshDebounceFrame:Show()
end

function EbonholdHub.SessionHistory.RefreshLogView()
    if not logViewVisible or not logScroll or not logChild or not logScrollFrame then return end

    local best = GetBestByNameCached()

    local scrollFrame = logScrollFrame
    local contentW = math.max((scrollFrame:GetWidth() or 0) - 4, 320)
    logChild:SetWidth(contentW)

    local session = GetActiveSession()
    local logs = session and session.logs or {}

    RefreshStatsBar(session, logs)

    if emptyLabel then
        if #logs == 0 then
            emptyLabel:SetText(session
                and "Auto pick actions will appear here.\nDetailed trace: /ebh debug"
                or "Start a run to begin logging.")
            emptyLabel:Show()
        else
            emptyLabel:Hide()
        end
    end

    if #logs == 0 then
        HideUnusedLogEntries(0)
        logChild:SetHeight(1)
        lastScrollPos = scrollFrame:GetVerticalScroll()
        return
    end

    local innerW = math.max(contentW - CONTENT_INSET * 2, 240)
    local slotW = math.max(88, math.floor((innerW - SLOT_GAP * 2) / 3))
    local total = #logs
    local totalH = total * ROW_H
    logChild:SetHeight(math.max(totalH, scrollFrame:GetHeight()))

    local scrollPos = scrollFrame:GetVerticalScroll()
    local viewH = scrollFrame:GetHeight() or 0
    local firstRow = math.floor(scrollPos / ROW_H)
    local visibleRows = math.ceil(viewH / ROW_H) + 1
    local lastRow = math.min(total - 1, firstRow + visibleRows)

    local poolIndex = 0
    for row = firstRow, lastRow do
        local logIndex = total - row
        if logIndex >= 1 and logIndex <= total then
            poolIndex = poolIndex + 1
            local card = EnsureLogEntry(poolIndex)
            local entry = logs[logIndex]

            card:ClearAllPoints()
            card:SetPoint("TOPLEFT", logChild, "TOPLEFT", 0, -(row * ROW_H))
            card:SetPoint("RIGHT", logChild, "RIGHT", 0, 0)
            card:SetHeight(ENTRY_H)

            PopulateLogCard(card, entry, slotW, best)
            card:Show()
        end
    end

    HideUnusedLogEntries(poolIndex)
    lastScrollPos = scrollPos
end

local function OnLogScrollChanged()
    if not logViewVisible or not logScrollFrame then return end
    local pos = logScrollFrame:GetVerticalScroll()
    if pos == lastScrollPos then return end
    EbonholdHub.SessionHistory.RefreshLogView()
end

local function HookLogScroll()
    if not logScrollFrame or logScrollFrame._ebhLogScrollHooked then return end
    logScrollFrame._ebhLogScrollHooked = true

    local prevWheel = logScrollFrame:GetScript("OnMouseWheel")
    logScrollFrame:SetScript("OnMouseWheel", function(self, delta)
        if prevWheel then prevWheel(self, delta) end
        OnLogScrollChanged()
    end)
end

local function StartScrollWatch()
    if not scrollWatchFrame then
        scrollWatchFrame = CreateFrame("Frame")
        scrollWatchFrame:Hide()
        scrollWatchFrame:SetScript("OnUpdate", function(self, dt)
            if not logViewVisible then
                self:Hide()
                return
            end
            self._elapsed = (self._elapsed or 0) + dt
            if self._elapsed < 0.10 then return end
            self._elapsed = 0
            OnLogScrollChanged()
        end)
    end
    scrollWatchFrame._elapsed = 0
    scrollWatchFrame:Show()
end

local function BuildUI(container)
    statsBar = CreateFrame("Frame", nil, container)
    statsBar:SetPoint("TOPLEFT", container, "TOPLEFT", 0, 0)
    statsBar:SetPoint("TOPRIGHT", container, "TOPRIGHT", 0, 0)
    statsBar:SetHeight(STATS_H)
    statsBar._cells = {}

    local statDefs = {
        { key = "picks", label = "Picks", value = "0" },
        { key = "last",  label = "Last",  value = "—" },
    }
    for i, def in ipairs(statDefs) do
        local cell = SW.CreateStatCell(statsBar, def.label, def.value)
        cell:SetHeight(STATS_H)
        statsBar._cells[i] = cell
        statLabels[def.key] = cell._valueLabel
    end
    statsBar:SetScript("OnSizeChanged", function() LayoutStatsBar(statsBar) end)

    toolbar = CreateFrame("Frame", nil, container)
    toolbar:SetPoint("TOPLEFT", statsBar, "BOTTOMLEFT", 0, -GAP)
    toolbar:SetPoint("TOPRIGHT", statsBar, "BOTTOMRIGHT", 0, -GAP)
    toolbar:SetHeight(TOOLBAR_H)

    local hintLabel = SW.Label(toolbar, "Auto pick history for your current run", 11, SC.textMuted)
    hintLabel:SetPoint("LEFT", toolbar, "LEFT", 0, 0)
    hintLabel:SetPoint("RIGHT", toolbar, "RIGHT", -76, 0)
    hintLabel:SetJustifyH("LEFT")

    clearBtn = SW.CreateOutlineButton(toolbar, "Clear", 68)
    clearBtn:SetPoint("RIGHT", toolbar, "RIGHT", 0, 0)
    clearBtn:SetScript("OnEnter", function(self)
        SW.SetBorderColor(self._border, DANGER)
    end)
    clearBtn:SetScript("OnLeave", function(self)
        SW.SetBorderColor(self._border, SC.border)
    end)
    clearBtn:SetScript("OnClick", function()
        StaticPopup_Show("EBONHOLDHUB_CLEAR_SESSIONS")
    end)

    logScroll, logChild = SW.CreatePlainScroll(container)
    logScroll:ClearAllPoints()
    logScroll:SetPoint("TOPLEFT", toolbar, "BOTTOMLEFT", 0, -GAP)
    logScroll:SetPoint("BOTTOMRIGHT", container, "BOTTOMRIGHT", 0, 0)
    logScrollFrame = logScroll
    HookLogScroll()

    logScroll:SetScript("OnSizeChanged", function()
        if not logViewVisible then return end
        local w = logScroll:GetWidth()
        if w and w > 0 and logChild then
            logChild:SetWidth(w)
        end
    end)

    emptyLabel = SW.Label(logChild, "", 12, SC.textMuted)
    emptyLabel:SetPoint("TOP", logChild, "TOP", 0, -48)
    emptyLabel:SetPoint("LEFT", logChild, "LEFT", 16, 0)
    emptyLabel:SetPoint("RIGHT", logChild, "RIGHT", -16, 0)
    emptyLabel:SetJustifyH("CENTER")
    emptyLabel:SetWordWrap(true)
end

function EbonholdHub.SessionHistory.Show(container)
    logViewVisible = true
    if not logScroll then
        BuildUI(container)
    else
        statsBar:SetParent(container)
        toolbar:SetParent(container)
        logScroll:SetParent(container)
        statsBar:Show()
        toolbar:Show()
        logScroll:Show()
    end

    lastScrollPos = -1
    LayoutStatsBar(statsBar)

    local defer = CreateFrame("Frame")
    defer:Hide()
    defer:SetScript("OnUpdate", function(self)
        self:Hide()
        if logViewVisible then
            EbonholdHub.SessionHistory.RefreshLogView()
        end
    end)
    defer:Show()
    StartScrollWatch()
end

function EbonholdHub.SessionHistory.Hide()
    logViewVisible = false
    EbonholdHub.SessionHistory.StopWork()
    if statsBar then statsBar:Hide() end
    if toolbar then toolbar:Hide() end
    if logScroll then logScroll:Hide() end
end

function EbonholdHub.SessionHistory.Init()
    StaticPopupDialogs["EBONHOLDHUB_CLEAR_SESSIONS"] = {
        text = "Clear the current run log? This cannot be undone.",
        button1 = "Yes", button2 = "No",
        OnAccept = function()
            EbonholdHub.Session.ClearAllSessions()
            EbonholdHub.SessionHistory.RefreshLogView()
        end,
        timeout = 0, whileDead = true, hideOnEscape = true,
    }
end
