-- SimWishlist.lua v4.0
-- Per-toon upgrade wishlist viewer. Boss tooltips, instance/boss grouping,
-- Upgrades by Slot, in-game import box, resizable window.

local ADDON_NAME = ...


local VERSION = "4.0"

-- ============================================================
-- Fetcher URL — Update this after deploying your Cloudflare Worker
-- Run: cd fetcher && npx wrangler deploy
-- Then replace YOURNAME below with your workers.dev subdomain
-- ============================================================
local FETCHER_URL = "https://simwishlist-fetcher.coadjuvant.workers.dev"

-- ============================================================
-- Changelog — shown in the "What's New" popup on version update
-- ============================================================
local CHANGELOG = {
  {
    version = "4.0",
    date = "2026-04-04",
    entries = {
      "Raidbots Droptimizer support via the SimWishlist Fetcher",
      "Item icons now display on every wishlist row",
      "Six-tier upgrade color coding",
      "Character switcher — view any imported character's wishlist",
      "Adventure Guide now opens correctly for all raids",
      "March on Quel'Danas raid added to loot database",
      "What's New changelog shown on version updates",
      "Fetcher URL shown on the Import page for quick access",
    },
  },
  {
    version = "3.0-3.5",
    date = "2026-03",
    entries = {
      "Boss sub-headers for raid tab with collapse/expand",
      "Draggable column resizing",
      "Tier set exclusion filter",
      "Best in Slot (BiS) tagging",
      "Auto-open after M+ completion",
    },
  },
  {
    version = "2.0-2.3",
    date = "2026-02",
    entries = {
      "Tabbed interface (Dungeon, Raid, Crafted, Delves, By Slot)",
      "Source grouping and instance auto-filter",
      "SWL! encoded import string support",
    },
  },
  {
    version = "1.0-1.3",
    date = "2026-01",
    entries = {
      "Initial release with QE Live import",
      "Resizable window with saved position",
      "Sort by upgrade, ilvl, or name",
    },
  },
}

-- ============================================================
-- Instance name → content type (matches GetInstanceInfo())
-- ============================================================
local INSTANCE_TO_TYPE = {
  -- Midnight Season 1 dungeons
  ["Windrunner Spire"]         = "DUNGEON",
  ["Magisters' Terrace"]       = "DUNGEON",
  ["Maisara Caverns"]          = "DUNGEON",
  ["Nexus-Point Xenas"]        = "DUNGEON",
  -- Legacy dungeons in rotation
  ["Algeth'ar Academy"]        = "DUNGEON",
  ["Pit of Saron"]             = "DUNGEON",
  ["Seat of the Triumvirate"]  = "DUNGEON",
  ["Skyreach"]                 = "DUNGEON",
  -- Midnight Season 1 raids (all three)
  ["The Voidspire"]            = "RAID",
  ["The Dreamrift"]            = "RAID",
  ["March on Quel'Danas"]      = "RAID",
}

-- Slot display order for "Upgrades by Slot" tab
local SLOT_ORDER = {
  "Head","Neck","Shoulder","Back","Chest","Wrist",
  "Hands","Waist","Legs","Feet","Finger","Trinket",
  "1H Weapon","Shield","Off-hand","2H Weapon","Ranged",
}

local TABS = { "DUNGEON", "RAID", "CRAFTED", "DELVES", "BY SLOT" }
local TAB_LABELS = {
  DUNGEON="Dungeon", RAID="Raid", CRAFTED="Crafted",
  DELVES="Delves", ["BY SLOT"]="By Slot",
}

local DEFAULT_SETTINGS = {
  sortMode   = "upgrade",  -- "upgrade" | "ilvl" | "name"
  minUpgrade = 0.01,
  windowPos  = { point="CENTER", x=0, y=0 },
  windowSize = { w=780, h=560 },
  lastTab    = "DUNGEON",
  colBoss    = 210,  -- boss column width
  colIlvl    = 42,   -- ilvl column width
  colUpg     = 72,   -- upgrade column width
}

-- ============================================================
-- State
-- ============================================================
local frame          = nil
local tabButtons     = {}
local rowPool        = {}
local hdrPool        = {}
local currentTab     = "DUNGEON"
local manualFilter   = nil   -- nil=auto, false=all, string=locked source
local settings       = nil
local importFrame    = nil
local whatsNewFrame  = nil
local CreateWhatsNewFrame  -- forward declaration (defined in What's New section)
local collapsedGroups = {}  -- [tabKey..source] = true if collapsed
local viewingChar     = nil -- nil=current character, "Name-Realm"=another

-- ============================================================
-- Per-toon helpers
-- ============================================================
local function CharKey()
  local name  = UnitName("player") or "Unknown"
  local realm = GetRealmName() or "Unknown"
  return name .. "-" .. realm
end

local function GetCharData()
  local key = CharKey()
  SimWishlistDB = SimWishlistDB or {}
  SimWishlistDB[key] = SimWishlistDB[key] or { items={} }
  return SimWishlistDB[key]
end

local function SetCharData(data)
  SimWishlistDB = SimWishlistDB or {}
  SimWishlistDB[CharKey()] = data
end

local function GetViewedCharKey()
  return viewingChar or CharKey()
end

local function GetViewedCharData()
  local key = GetViewedCharKey()
  SimWishlistDB = SimWishlistDB or {}
  SimWishlistDB[key] = SimWishlistDB[key] or { items={} }
  return SimWishlistDB[key]
end

-- ============================================================
-- Settings (per-account in SavedVariablesPerCharacter)
-- ============================================================
local function LoadSettings()
  SimWishlistCharDB = SimWishlistCharDB or {}
  settings = SimWishlistCharDB.settings or {}
  for k, v in pairs(DEFAULT_SETTINGS) do
    if settings[k] == nil then
      if type(v) == "table" then
        settings[k] = {}
        for k2,v2 in pairs(v) do settings[k][k2] = v2 end
      else
        settings[k] = v
      end
    end
  end
  SimWishlistCharDB.settings = settings
end

local function SaveSettings()
  if SimWishlistCharDB then
    SimWishlistCharDB.settings = settings
  end
end

-- ============================================================
-- LootDB lookup
-- ============================================================
local INVTYPE_TO_SLOT = {
  INVTYPE_HEAD          = "Head",
  INVTYPE_NECK          = "Neck",
  INVTYPE_SHOULDER      = "Shoulder",
  INVTYPE_CLOAK         = "Back",
  INVTYPE_CHEST         = "Chest",
  INVTYPE_ROBE          = "Chest",
  INVTYPE_WRIST         = "Wrist",
  INVTYPE_HAND          = "Hands",
  INVTYPE_WAIST         = "Waist",
  INVTYPE_LEGS          = "Legs",
  INVTYPE_FEET          = "Feet",
  INVTYPE_FINGER        = "Finger",
  INVTYPE_TRINKET       = "Trinket",
  INVTYPE_WEAPON        = "1H Weapon",
  INVTYPE_WEAPONMAINHAND= "1H Weapon",
  INVTYPE_WEAPONOFFHAND = "1H Weapon",
  INVTYPE_SHIELD        = "Shield",
  INVTYPE_HOLDABLE      = "Off-hand",
  INVTYPE_2HWEAPON      = "2H Weapon",
  INVTYPE_RANGED        = "Ranged",
  INVTYPE_RANGEDRIGHT   = "Ranged",
  INVTYPE_THROWN        = "Ranged",
}

local function GetItemSlot(itemID)
  local _, _, _, _, _, _, _, _, invType = GetItemInfo(itemID)
  if invType and invType ~= "" then
    return INVTYPE_TO_SLOT[invType] or invType
  end
  return nil
end

local function GetLootInfo(itemID)
  if SimWishlistLootDB and itemID then
    return SimWishlistLootDB[tonumber(itemID)]
  end
  return nil
end

-- ============================================================
-- Instance detection
-- ============================================================
local function GetCurrentInstance()
  local name, _, instanceType = GetInstanceInfo()
  if not name or name == "" or instanceType == "none" then
    return nil, nil
  end
  local mapped = INSTANCE_TO_TYPE[name]
  if mapped then return name, mapped end
  if instanceType == "raid"  then return name, "RAID"    end
  if instanceType == "party" then return name, "DUNGEON" end
  return nil, nil
end

-- ============================================================
-- Data helpers
-- ============================================================
local function GetItemsForTab(tabKey)
  local data = GetViewedCharData()
  if not data.items then return {} end
  local out = {}
  for _, item in ipairs(data.items) do
    local loot = GetLootInfo(item.itemID)
    local loc  = (item.dropLoc or ""):upper()
    if tabKey == "BY SLOT" then
      -- always included
      out[#out+1] = item
    elseif tabKey == "DUNGEON" and loc == "DUNGEON" then
      out[#out+1] = item
    elseif tabKey == "RAID" and loc == "RAID" then
      out[#out+1] = item
    elseif tabKey == "CRAFTED" and loc == "CRAFTED" then
      out[#out+1] = item
    elseif tabKey == "DELVES" and loc == "DELVES" then
      out[#out+1] = item
    end
  end
  return out
end

local function FilterBySource(items, sourceFilter)
  if not sourceFilter then return items end
  local out = {}
  for _, item in ipairs(items) do
    local loot = GetLootInfo(item.itemID)
    local src  = (loot and loot.instance) or (item.dropLoc or "Unknown")
    if src == sourceFilter then out[#out+1] = item end
  end
  return out
end

local function FilterByMinUpgrade(items)
  local min = settings.minUpgrade or 0
  if min <= 0 then return items end
  local out = {}
  for _, item in ipairs(items) do
    if (item.upgrade or 0) >= min then out[#out+1] = item end
  end
  return out
end

-- ============================================================
-- Tier Set exclusion (by item name substring)
-- ============================================================
local TIER_SET_KEYWORDS = {
  "Relentless Rider's",    -- DK
  "Devouring Reaver's",    -- DH
  "Luminous Bloom",         -- Druid
  "Black Talon",            -- Evoker
  "Primal Sentry's",       -- Hunter
  "Voidbreaker's",          -- Mage
  "Ra-den's Chosen",       -- Monk
  "Luminant Verdict's",    -- Paladin
  "Blind Oath's",           -- Priest
  "Grim Jest",              -- Rogue
  "Primal Core",            -- Shaman
  "Abyssal Immolator",     -- Warlock
  "Night Ender",            -- Warrior
}

local function IsTierSetByName(itemID)
  local name = GetItemInfo(itemID)
  if not name then return false end
  for _, keyword in ipairs(TIER_SET_KEYWORDS) do
    if name:find(keyword, 1, true) then return true end
  end
  return false
end

local function FilterOutTierSets(items)
  local out = {}
  for _, item in ipairs(items) do
    if not IsTierSetByName(item.itemID) then
      out[#out+1] = item
    end
  end
  return out
end

local function SortItems(items)
  local mode = settings.sortMode or "upgrade"
  table.sort(items, function(a, b)
    if mode == "ilvl" then
      if a.ilvl ~= b.ilvl then return (a.ilvl or 0) > (b.ilvl or 0) end
    elseif mode == "name" then
      local an = GetItemInfo(a.itemID) or ("Item#"..tostring(a.itemID))
      local bn = GetItemInfo(b.itemID) or ("Item#"..tostring(b.itemID))
      if an ~= bn then return an < bn end
    end
    -- default / tiebreak: upgrade desc
    return (a.upgrade or 0) > (b.upgrade or 0)
  end)
end

local function GroupBySource(items)
  -- Returns ordered list of { source, boss?, items[] }
  local groups = {}
  local map    = {}
  for _, item in ipairs(items) do
    local loot = GetLootInfo(item.itemID)
    local src  = (loot and loot.instance) or (item.dropLoc or "Unknown")
    if not map[src] then
      map[src] = { source=src, items={} }
      groups[#groups+1] = map[src]
    end
    map[src].items[#map[src].items+1] = item
  end
  -- Sort groups by best upgrade in group desc
  table.sort(groups, function(a, b)
    local ab, bb = 0, 0
    for _, i in ipairs(a.items) do if (i.upgrade or 0) > ab then ab = i.upgrade end end
    for _, i in ipairs(b.items) do if (i.upgrade or 0) > bb then bb = i.upgrade end end
    return ab > bb
  end)
  return groups
end


local function GroupByBoss(items)
  -- Returns ordered list of { boss, items[] }
  local groups = {}
  local map    = {}
  for _, item in ipairs(items) do
    local loot = GetLootInfo(item.itemID)
    local boss = (loot and loot.boss) or "Uncategorized"
    if not map[boss] then
      map[boss] = { boss=boss, items={} }
      groups[#groups+1] = map[boss]
    end
    map[boss].items[#map[boss].items+1] = item
  end
  table.sort(groups, function(a, b)
    if a.boss == "Uncategorized" then return false end
    if b.boss == "Uncategorized" then return true end
    local ab, bb = 0, 0
    for _, i in ipairs(a.items) do if (i.upgrade or 0) > ab then ab = i.upgrade end end
    for _, i in ipairs(b.items) do if (i.upgrade or 0) > bb then bb = i.upgrade end end
    return ab > bb
  end)
  return groups
end

local function GroupBySlot(items)
  local groups = {}
  local map    = {}
  for _, item in ipairs(items) do
    local loot = GetLootInfo(item.itemID)
    local slot = GetItemSlot(item.itemID) or (loot and loot.slot) or "Other"
    if not map[slot] then
      map[slot] = { source=slot, items={} }
      groups[#groups+1] = map[slot]
    end
    map[slot].items[#map[slot].items+1] = item
  end
  -- Sort by SLOT_ORDER
  local order = {}
  for i, s in ipairs(SLOT_ORDER) do order[s] = i end
  table.sort(groups, function(a, b)
    local ai = order[a.source] or 99
    local bi = order[b.source] or 99
    return ai < bi
  end)
  return groups
end

-- ============================================================
-- Formatting
-- ============================================================
local function FmtUpgrade(val)
  val = tonumber(val) or 0
  if val == 0 then return "|cff999999+0.00%|r" end
  local color = val >= 5.0 and "ff8800"
             or val >= 3.0 and "44ff88"
             or val >= 2.0 and "aaff44"
             or val >= 1.0 and "ffee00"
             or val >= 0.5 and "ffffff"
             or "aaaaaa"
  return string.format("|cff%s+%.2f%%|r", color, val)
end

-- ============================================================
-- BiS (Best in Slot) computation
-- ============================================================
-- Maps slot name → itemID of the highest-upgrade item for that slot.
-- Computed across ALL imported items regardless of tab/filter.
local bisMap = {}  -- { [slotName] = itemID }

local function ComputeBiSMap()
  bisMap = {}
  local data = GetViewedCharData()
  if not data.items then return end

  local bestBySlot = {}  -- { [slot] = { itemID, upgrade } }
  for _, item in ipairs(data.items) do
    if not IsTierSetByName(item.itemID) then
      local slot = GetItemSlot(item.itemID)
      if slot then
        local upg = item.upgrade or 0
        if upg > 0 and (not bestBySlot[slot] or upg > bestBySlot[slot].upgrade) then
          bestBySlot[slot] = { itemID = item.itemID, upgrade = upg }
        end
      end
    end
  end

  for slot, info in pairs(bestBySlot) do
    bisMap[info.itemID] = true
  end
end

local function IsBiS(itemID)
  return bisMap[itemID] == true
end

local BIS_TAG = " |cff00ff00[BiS]|r"

-- ============================================================
-- Import parsing
-- ============================================================
-- Accepts SWL! encoded strings or raw JSON and stores results
-- into the current toon's data.

local importCallbacks = {}

local function ParseAndStoreJSON(jsonStr)
  -- Parses the SimWishlist JSON format
  local ok, result = pcall(function()
    local items = {}
    for entry in jsonStr:gmatch('%{[^{}]+%}') do
      local itemID   = tonumber(entry:match('"item"%s*:%s*(%d+)'))
      local dropLoc  = entry:match('"dropLoc"%s*:%s*"([^"]+)"')
      local level    = tonumber(entry:match('"level"%s*:%s*(%d+)'))
      local percDiff = tonumber(entry:match('"percDiff"%s*:%s*([%d%.%-]+)'))
      if itemID and dropLoc then
        items[#items+1] = {
          itemID   = itemID,
          dropLoc  = dropLoc:upper(),
          ilvl     = level or 0,
          upgrade  = percDiff or 0,
        }
      end
    end
    local spec      = jsonStr:match('"spec"%s*:%s*"([^"]+)"')
    local player    = jsonStr:match('"playername"%s*:%s*"([^"]+)"')
    local realm     = jsonStr:match('"realm"%s*:%s*"([^"]+)"')
    local reportId  = jsonStr:match('"id"%s*:%s*"([^"]+)"')
    return { items=items, spec=spec, player=player, realm=realm, reportId=reportId }
  end)
  if not ok or not result then return false, "Parse failed" end
  if #result.items == 0 then return false, "No items found in response" end

  local charData = {
    items      = result.items,
    spec       = result.spec,
    player     = result.player,
    realm      = result.realm,
    reportId   = result.reportId,
    source     = "SimWishlist",
    importTime = time(),
  }
  SetCharData(charData)
  return true, #result.items
end

local function Base64Decode(data)
  local lookup = {}
  local chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
  for i = 1, #chars do
    lookup[chars:sub(i,i)] = i - 1
  end
  -- Strip non-base64 chars
  local cleaned = data:gsub('[^ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=]', '')
  local result = {}
  local i = 1
  while i <= #cleaned do
    local c1 = lookup[cleaned:sub(i,i)]   or 0
    local c2 = lookup[cleaned:sub(i+1,i+1)] or 0
    local c3 = lookup[cleaned:sub(i+2,i+2)]
    local c4 = lookup[cleaned:sub(i+3,i+3)]
    result[#result+1] = string.char(c1 * 4 + math.floor(c2 / 16))
    if cleaned:sub(i+2,i+2) ~= '=' and c3 then
      result[#result+1] = string.char((c2 % 16) * 16 + math.floor(c3 / 4))
    end
    if cleaned:sub(i+3,i+3) ~= '=' and c4 then
      result[#result+1] = string.char((c3 % 4) * 64 + c4)
    end
    i = i + 4
  end
  return table.concat(result)
end

local function ParseSWLString(input)
  local encoded = input:match('^SWL!(.+)$')
  if not encoded then return false, 'Not a SWL! string' end
  local ok, csv = pcall(Base64Decode, encoded)
  if not ok or not csv then return false, 'Decode failed' end
  local items = {}
  local meta = {}
  for line in csv:gmatch('[^\n]+') do
    local parts = {}
    for p in (line .. '|'):gmatch('([^|]*)|') do
      parts[#parts+1] = p
    end
    if parts[1] == 'H' then
      meta.reportId = parts[2]
      meta.spec     = parts[3]
      meta.player   = parts[4]
      meta.realm    = parts[5]
      meta.source   = parts[6] or 'SimWishlist'
    elseif parts[1] == 'I' then
      local itemID = tonumber(parts[2])
      if itemID then
        items[#items+1] = {
          itemID  = itemID,
          dropLoc = parts[3] or 'DUNGEON',
          ilvl    = tonumber(parts[4]) or 0,
          upgrade = tonumber(parts[5]) or 0,
        }
      end
    end
  end
  if #items == 0 then return false, 'No items in SWL string' end
  SetCharData({
    items      = items,
    spec       = meta.spec,
    player     = meta.player,
    realm      = meta.realm,
    reportId   = meta.reportId,
    source     = meta.source or 'SimWishlist',
    importTime = time(),
  })
  return true, #items
end

local function DoImport(input)
  input = strtrim(input or '')
  if input == '' then return false, 'Empty input' end

  -- SWL! encoded string
  if input:sub(1,4) == 'SWL!' then
    return ParseSWLString(input)
  end

  -- Strip outer quotes if JSON was wrapped in a quoted string
  if input:sub(1,1) == '"' then
    input = input:sub(2)
    if input:sub(-1) == '"' then input = input:sub(1,-2) end
    input = input:gsub('\\"', '"')
  end

  -- Raw JSON
  if input:sub(1,1) == '{' then
    return ParseAndStoreJSON(input)
  end

  return false, 'Expected SWL! string or JSON. Got: ' .. input:sub(1,40)
end


-- ============================================================
-- Import frame
-- ============================================================
local function CreateImportFrame()
  if importFrame then
    importFrame.editBox:SetText("")
    importFrame.statusLbl:SetText("")
    importFrame:Show()
    return
  end

  local f = CreateFrame("Frame", "SimWishlistImportFrame", UIParent, "BackdropTemplate")
  f:SetSize(540, 340)
  f:SetPoint("CENTER")
  f:SetMovable(true)
  f:EnableMouse(true)
  f:RegisterForDrag("LeftButton")
  f:SetScript("OnDragStart", f.StartMoving)
  f:SetScript("OnDragStop", f.StopMovingOrSizing)
  f:SetClampedToScreen(true)
  f:SetFrameStrata("DIALOG")

  f:SetBackdrop({
    bgFile="Interface/Tooltips/UI-Tooltip-Background",
    edgeFile="Interface/Tooltips/UI-Tooltip-Border",
    tile=true, tileSize=16, edgeSize=16,
    insets={left=4,right=4,top=4,bottom=4}
  })
  f:SetBackdropColor(0.05, 0.05, 0.12, 0.98)
  f:SetBackdropBorderColor(0.5, 0.35, 0.8, 1)

  local title = f:CreateFontString(nil,"OVERLAY","GameFontNormal")
  title:SetPoint("TOP", f, "TOP", 0, -12)
  title:SetText("|cffcc88ffSim Wishlist — Import|r")

  local close = CreateFrame("Button",nil,f,"UIPanelCloseButton")
  close:SetPoint("TOPRIGHT",f,"TOPRIGHT",2,2)
  close:SetScript("OnClick", function() f:Hide() end)

  local instructions = f:CreateFontString(nil,"OVERLAY","GameFontNormalSmall")
  instructions:SetPoint("TOPLEFT",f,"TOPLEFT",12,-34)
  instructions:SetPoint("TOPRIGHT",f,"TOPRIGHT",-12,-34)
  instructions:SetJustifyH("LEFT")
  instructions:SetWordWrap(true)
  instructions:SetText(
    "|cffffff88Paste one of the following:|r\n" ..
    "  • SWL! string from the fetcher (QE Live or Raidbots)\n" ..
    "  • Raw JSON from QE Live\n\n" ..
    "|cffaaaaaa" .. CharKey() .. "|r"
  )

  -- Fetcher URL (copy and open in your browser)
  local fetcherLbl = f:CreateFontString(nil,"OVERLAY","GameFontNormalSmall")
  fetcherLbl:SetPoint("TOPLEFT", f, "TOPLEFT", 12, -96)
  fetcherLbl:SetText("|cffffff88Copy this URL and open in your browser to get import strings:|r")

  local fetcherBox = CreateFrame("EditBox", nil, f, "InputBoxTemplate")
  fetcherBox:SetPoint("TOPLEFT", f, "TOPLEFT", 16, -112)
  fetcherBox:SetPoint("TOPRIGHT", f, "TOPRIGHT", -16, -112)
  fetcherBox:SetHeight(22)
  fetcherBox:SetAutoFocus(false)
  fetcherBox:SetFontObject("ChatFontNormal")
  fetcherBox:SetText(FETCHER_URL)
  fetcherBox:SetScript("OnEditFocusGained", function(self) self:HighlightText() end)
  fetcherBox:SetScript("OnEscapePressed", function(self) self:ClearFocus() end)
  fetcherBox:SetScript("OnTextChanged", function(self, userInput)
    if userInput then self:SetText(FETCHER_URL); self:HighlightText() end
  end)
  f.fetcherBox = fetcherBox

  -- Paste box label
  local pasteLbl = f:CreateFontString(nil,"OVERLAY","GameFontNormalSmall")
  pasteLbl:SetPoint("TOPLEFT", f, "TOPLEFT", 12, -140)
  pasteLbl:SetText("|cffffff88Paste your import string below:|r")

  local editBox = CreateFrame("EditBox", nil, f, "InputBoxTemplate")
  editBox:SetPoint("TOPLEFT",  f, "TOPLEFT",  16, -155)
  editBox:SetPoint("TOPRIGHT", f, "TOPRIGHT", -16, -155)
  editBox:SetHeight(80)
  editBox:SetMultiLine(true)
  editBox:SetAutoFocus(false)
  editBox:SetFontObject("ChatFontNormal")
  editBox:SetScript("OnEscapePressed", function(self) self:ClearFocus() end)
  f.editBox = editBox

  local scrollFrame = CreateFrame("ScrollFrame", nil, f, "UIPanelScrollFrameTemplate")
  scrollFrame:SetPoint("TOPLEFT",  f, "TOPLEFT",  16, -155)
  scrollFrame:SetPoint("TOPRIGHT", f, "TOPRIGHT", -30, -155)
  scrollFrame:SetHeight(80)
  scrollFrame:SetScrollChild(editBox)
  editBox:SetWidth(490)

  local statusLbl = f:CreateFontString(nil,"OVERLAY","GameFontNormalSmall")
  statusLbl:SetPoint("BOTTOMLEFT", f,"BOTTOMLEFT", 16, 44)
  statusLbl:SetPoint("BOTTOMRIGHT",f,"BOTTOMRIGHT",-120, 44)
  statusLbl:SetJustifyH("LEFT")
  statusLbl:SetText("")
  f.statusLbl = statusLbl

  local importBtn = CreateFrame("Button",nil,f,"UIPanelButtonTemplate")
  importBtn:SetSize(100, 24)
  importBtn:SetPoint("BOTTOMRIGHT",f,"BOTTOMRIGHT",-12,12)
  importBtn:SetText("Import")
  importBtn:SetScript("OnClick", function()
    local input = strtrim(editBox:GetText() or "")
    if input == "" then
      statusLbl:SetText("|cffff8888Paste a URL or JSON first.|r")
      return
    end
    local ok, result = DoImport(input)
    if ok then
      viewingChar = nil
      statusLbl:SetText("|cff44ff88Imported " .. tostring(result) .. " items for " .. CharKey() .. ".|r")
      editBox:SetText("")
      if frame and frame:IsShown() then SimWishlist_Refresh() end
    else
      statusLbl:SetText("|cffff8888Error: " .. tostring(result) .. "|r")
    end
  end)

  local clearBtn = CreateFrame("Button",nil,f,"UIPanelButtonTemplate")
  clearBtn:SetSize(80, 24)
  clearBtn:SetPoint("RIGHT", importBtn, "LEFT", -8, 0)
  clearBtn:SetText("Clear Data")
  clearBtn:SetScript("OnClick", function()
    SetCharData({ items={} })
    statusLbl:SetText("|cffaaaaaa Data cleared for " .. CharKey() .. ".|r")
    if frame and frame:IsShown() then SimWishlist_Refresh() end
  end)

  importFrame = f
  f:Show()
end

-- ============================================================
-- Main frame construction
-- ============================================================
local ROW_H  = 26
local HDR_H  = 22
local PAD    = 3
local HDR_GAP = 10  -- extra space above each section header

-- ============================================================
-- Column layout
-- ============================================================
function SimWishlist_LayoutColumns()
  if not frame or not settings then return end
  local cW = frame.colBar and frame.colBar:GetWidth() or (frame:GetWidth() - 36)
  if not cW or cW <= 0 then return end
  local colUpg  = settings.colUpg  or 58
  local colIlvl = settings.colIlvl or 36
  local colBoss = settings.colBoss or 140

  if frame.colUpgLbl then
    frame.colUpgLbl:ClearAllPoints()
    frame.colUpgLbl:SetPoint("RIGHT", frame.colBar, "RIGHT", 0, 0)
    frame.colUpgLbl:SetWidth(colUpg)
  end
  if frame.colIlvlLbl then
    frame.colIlvlLbl:ClearAllPoints()
    frame.colIlvlLbl:SetPoint("RIGHT", frame.colBar, "RIGHT", -colUpg, 0)
    frame.colIlvlLbl:SetWidth(colIlvl)
  end
  if frame.colBossLbl then
    frame.colBossLbl:ClearAllPoints()
    frame.colBossLbl:SetPoint("RIGHT", frame.colBar, "RIGHT", -(colUpg + colIlvl), 0)
    frame.colBossLbl:SetWidth(colBoss)
  end
  if frame.colName then
    frame.colName:ClearAllPoints()
    frame.colName:SetPoint("LEFT", frame.colBar, "LEFT", 30, 0)
    frame.colName:SetPoint("RIGHT", frame.colBar, "RIGHT", -(colUpg + colIlvl + colBoss + 4), 0)
  end
  if frame.dragBossIlvl then
    frame.dragBossIlvl:ClearAllPoints()
    frame.dragBossIlvl:SetPoint("RIGHT", frame.colBar, "RIGHT", -(colUpg + colIlvl), 0)
  end
  if frame.dragIlvlUpg then
    frame.dragIlvlUpg:ClearAllPoints()
    frame.dragIlvlUpg:SetPoint("RIGHT", frame.colBar, "RIGHT", -colUpg, 0)
  end
  -- Update existing rows
  for _, row in pairs(rowPool) do
    if row.bossLbl then
      row.bossLbl:ClearAllPoints()
      row.bossLbl:SetWidth(colBoss)
      row.bossLbl:SetPoint("RIGHT", row, "RIGHT", -(colUpg + colIlvl + 4), 0)
    end
    if row.ilvlLbl then
      row.ilvlLbl:ClearAllPoints()
      row.ilvlLbl:SetWidth(colIlvl)
      row.ilvlLbl:SetPoint("RIGHT", row, "RIGHT", -colUpg, 0)
    end
    if row.upgLbl then
      row.upgLbl:ClearAllPoints()
      row.upgLbl:SetWidth(colUpg)
      row.upgLbl:SetPoint("RIGHT", row, "RIGHT", 0, 0)
    end
    if row.nameLbl then
      row.nameLbl:ClearAllPoints()
      row.nameLbl:SetPoint("LEFT", row, "LEFT", 30, 0)
      row.nameLbl:SetPoint("RIGHT", row, "RIGHT", -(colUpg + colIlvl + colBoss + 8), 0)
    end
  end
end

local function NewRow(parent)
  local row = CreateFrame("Button", nil, parent)
  row:SetHeight(ROW_H)

  local bg = row:CreateTexture(nil,"BACKGROUND")
  bg:SetAllPoints(); bg:SetColorTexture(0,0,0,0)
  row.bg = bg

  local dot = row:CreateTexture(nil,"ARTWORK")
  dot:SetSize(3,14); dot:SetPoint("LEFT",row,"LEFT",0,0)
  dot:SetColorTexture(0.5,0.3,0.85,0.7)
  row.dot = dot

  local icon = row:CreateTexture(nil,"ARTWORK")
  icon:SetSize(22,22); icon:SetPoint("LEFT",row,"LEFT",4,0)
  icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
  row.icon = icon

  local nameLbl = row:CreateFontString(nil,"OVERLAY","GameFontNormal")  -- Normal = bold
  nameLbl:SetPoint("LEFT",row,"LEFT",30,0)
  nameLbl:SetPoint("RIGHT",row,"RIGHT",-150,0)
  nameLbl:SetJustifyH("LEFT"); nameLbl:SetWordWrap(false)
  row.nameLbl = nameLbl

  local bossLbl = row:CreateFontString(nil,"OVERLAY","GameFontNormalSmall")
  bossLbl:SetJustifyH("RIGHT"); bossLbl:SetTextColor(0.65,0.65,0.75,1)
  row.bossLbl = bossLbl

  local ilvlLbl = row:CreateFontString(nil,"OVERLAY","GameFontNormalSmall")
  ilvlLbl:SetJustifyH("CENTER")
  row.ilvlLbl = ilvlLbl

  local upgLbl = row:CreateFontString(nil,"OVERLAY","GameFontNormalSmall")
  upgLbl:SetJustifyH("RIGHT")
  row.upgLbl = upgLbl

  row:SetScript("OnEnter", function(self)
    self.bg:SetColorTexture(0.20,0.15,0.35,0.6)
    local item = self.itemData
    if not item then return end
    local loot = GetLootInfo(item.itemID)
    GameTooltip:SetOwner(self,"ANCHOR_RIGHT")
    local name, link = GetItemInfo(item.itemID)
    if link then
      GameTooltip:SetHyperlink(link)
      GameTooltip:AddLine(" ")
    else
      GameTooltip:AddLine(name or ("Item #"..tostring(item.itemID)), 1,1,1)
    end
    if loot then
      GameTooltip:AddLine("Location: " .. (loot.instance or "?"), 0.8,0.65,1)
      GameTooltip:AddLine("Source: " .. (loot.boss or "?"), 0.6,0.85,1)
      GameTooltip:AddLine("Slot: " .. (GetItemSlot(item.itemID) or loot.slot or "?"), 0.7,0.7,0.7)
    end
    GameTooltip:AddLine(string.format("Upgrade: +%.2f%%", item.upgrade or 0), 0.4,1,0.5)
    if IsBiS(item.itemID) then
      GameTooltip:AddLine("Best in Slot upgrade", 0,1,0)
    end
    GameTooltip:AddLine("|cffaaaaaa[Click to open Adventure Guide]|r", 0.6,0.6,0.6)
    GameTooltip:Show()
  end)
  row:SetScript("OnLeave", function(self)
    local c = self.altRow and 0.06 or 0
    self.bg:SetColorTexture(c,c,c*2, self.altRow and 0.3 or 0)
    GameTooltip:Hide()
  end)
  row:SetScript("OnClick", function(self, btn)
    if btn ~= "LeftButton" then return end
    local item = self.itemData
    if not item or not item.itemID then return end
    local itemID = item.itemID
    local loot = GetLootInfo(itemID)

    if C_AddOns and C_AddOns.LoadAddOn then
      C_AddOns.LoadAddOn("Blizzard_EncounterJournal")
    elseif LoadAddOn then
      LoadAddOn("Blizzard_EncounterJournal")
    end

    C_Timer.After(0.05, function()
      if not EncounterJournal_OpenJournal then return end

      if loot and loot.instance and loot.boss then
        local targetInst = loot.instance
        local targetBoss = loot.boss
        local journalInstID, journalEncID

        -- Iterate all expansion tiers (latest first) so we always
        -- find the correct instance regardless of prior journal state.
        local numTiers = EJ_GetNumTiers and EJ_GetNumTiers() or 0
        for tierIdx = numTiers, 1, -1 do
          EJ_SelectTier(tierIdx)
          for _, isRaid in ipairs({false, true}) do
            local idx = 1
            while true do
              local instID, instName = EJ_GetInstanceByIndex(idx, isRaid)
              if not instID then break end
              if instName == targetInst then
                EJ_SelectInstance(instID)
                local encIdx = 1
                while true do
                  -- Returns: name, description, journalEncounterID, rootSectionID,
                  --          link, journalInstanceID, dungeonEncounterID, instanceID
                  local eName, _, encID, _, _, jInstID = EJ_GetEncounterInfoByIndex(encIdx)
                  if not eName then break end
                  if eName == targetBoss then
                    journalInstID = jInstID or instID
                    journalEncID  = encID
                    break
                  end
                  encIdx = encIdx + 1
                end
                break
              end
              idx = idx + 1
            end
            if journalEncID then break end
          end
          if journalEncID then break end
        end

        if journalEncID then
          pcall(EncounterJournal_OpenJournal, nil, journalInstID, journalEncID, nil, nil, itemID)
          return
        end
      end

      -- Fallback
      pcall(EncounterJournal_OpenJournal, nil, nil, nil, nil, nil, itemID)
    end)
  end)
  row:RegisterForClicks("LeftButtonUp", "RightButtonUp")
  return row
end

local function GetRow(idx, parent)
  if not rowPool[idx] then rowPool[idx] = NewRow(parent) end
  return rowPool[idx]
end

local function NewHeader(parent)
  local h = CreateFrame("Button", nil, parent)
  h:SetHeight(HDR_H)
  local bg = h:CreateTexture(nil,"BACKGROUND")
  bg:SetAllPoints(); bg:SetColorTexture(0.12,0.08,0.25,1)
  h.bg = bg

  -- Collapse arrow
  local arrow = h:CreateFontString(nil,"OVERLAY","GameFontNormalSmall")
  arrow:SetPoint("LEFT",h,"LEFT",4,0)
  arrow:SetWidth(14); arrow:SetJustifyH("CENTER")
  arrow:SetTextColor(0.7,0.6,0.9,1)
  arrow:SetText("-")
  h.arrow = arrow

  local lbl = h:CreateFontString(nil,"OVERLAY","GameFontNormalSmall")
  lbl:SetPoint("LEFT",h,"LEFT",20,0)
  lbl:SetPoint("RIGHT",h,"RIGHT",-58,0)
  lbl:SetJustifyH("LEFT"); lbl:SetTextColor(0.85,0.65,1,1)
  h.lbl = lbl

  local filterBtn = CreateFrame("Button",nil,h)
  filterBtn:SetSize(50,18); filterBtn:SetPoint("RIGHT",h,"RIGHT",-4,0)
  filterBtn:SetNormalFontObject("GameFontNormalSmall")
  filterBtn:SetText("|cff88aaff[Only]|r")
  filterBtn:SetScript("OnEnter", function(self)
    GameTooltip:SetOwner(self,"ANCHOR_RIGHT")
    GameTooltip:SetText("Filter to this source only",1,1,1)
    GameTooltip:Show()
  end)
  filterBtn:SetScript("OnLeave", function() GameTooltip:Hide() end)
  h.filterBtn = filterBtn

  -- Click header to collapse/expand
  h:SetScript("OnClick", function(self)
    if self.collapseKey then
      collapsedGroups[self.collapseKey] = not collapsedGroups[self.collapseKey]
      SimWishlist_Refresh()
    end
  end)
  h:SetScript("OnEnter", function(self)
    self.bg:SetColorTexture(0.18,0.12,0.32,1)
  end)
  h:SetScript("OnLeave", function(self)
    self.bg:SetColorTexture(0.12,0.08,0.25,1)
  end)

  return h
end

local function GetHeader(idx, parent)
  if not hdrPool[idx] then hdrPool[idx] = NewHeader(parent) end
  return hdrPool[idx]
end

local function HidePool(pool)
  for _,v in pairs(pool) do v:Hide() end
end

local function ShowCharSwitcher(anchorFrame)
  MenuUtil.CreateContextMenu(anchorFrame, function(ownerRegion, rootDescription)
    rootDescription:CreateTitle("Switch Character")

    local currentKey = CharKey()
    rootDescription:CreateButton("Current Character (" .. currentKey .. ")", function()
      viewingChar = nil
      SimWishlist_Refresh()
    end)

    rootDescription:CreateDivider()

    for key, data in pairs(SimWishlistDB or {}) do
      if type(data) == "table" and data.items then
        local label = key
        if key == GetViewedCharKey() then
          label = "|cff44ff88> " .. key .. "|r"
        end
        rootDescription:CreateButton(label, function()
          if key == currentKey then
            viewingChar = nil
          else
            viewingChar = key
          end
          SimWishlist_Refresh()
        end)
      end
    end
  end)
end

local function CreateMainFrame()
  local f = CreateFrame("Frame","SimWishlistFrame",UIParent,"BackdropTemplate")
  f:SetMovable(true); f:SetResizable(true)
  f:SetResizeBounds(500, 380)
  f:EnableMouse(true)
  f:RegisterForDrag("LeftButton")
  f:SetScript("OnDragStart", f.StartMoving)
  f:SetScript("OnDragStop", function(self)
    self:StopMovingOrSizing()
    local p,_,_,x,y = self:GetPoint(1)
    settings.windowPos = {point=p, x=x, y=y}
    SaveSettings()
  end)
  f:SetClampedToScreen(true)
  f:SetFrameStrata("HIGH")
  f:Hide()

  f:SetBackdrop({
    bgFile="Interface/Tooltips/UI-Tooltip-Background",
    edgeFile="Interface/Tooltips/UI-Tooltip-Border",
    tile=true, tileSize=16, edgeSize=16,
    insets={left=4,right=4,top=4,bottom=4}
  })
  f:SetBackdropColor(0.05,0.05,0.10,0.96)
  f:SetBackdropBorderColor(0.45,0.32,0.72,1)

  -- Restore size/pos
  local sz = settings.windowSize
  f:SetSize(sz.w or 700, sz.h or 560)
  local pos = settings.windowPos
  f:SetPoint(pos.point or "CENTER", UIParent, pos.point or "CENTER", pos.x or 0, pos.y or 0)

  -- Title bar
  local bar = f:CreateTexture(nil,"BACKGROUND")
  bar:SetPoint("TOPLEFT",f,"TOPLEFT",4,-4)
  bar:SetPoint("TOPRIGHT",f,"TOPRIGHT",-4,-4)
  bar:SetHeight(22); bar:SetColorTexture(0.12,0.08,0.28,1)

  local titleLbl = f:CreateFontString(nil,"OVERLAY","GameFontNormalSmall")
  titleLbl:SetPoint("LEFT",bar,"LEFT",8,0)
  titleLbl:SetText("|cffcc88ffSim Wishlist|r  |cff666666v"..VERSION.."|r")

  local charBtn = CreateFrame("Button", nil, f)
  charBtn:SetPoint("CENTER", bar, "CENTER", 0, 0)
  charBtn:SetSize(200, 20)
  charBtn:RegisterForClicks("LeftButtonUp")
  charBtn:SetScript("OnClick", function(self) ShowCharSwitcher(self) end)
  charBtn:SetScript("OnEnter", function(self)
    GameTooltip:SetOwner(self, "ANCHOR_BOTTOM")
    GameTooltip:SetText("Click to switch character", 1, 1, 1)
    GameTooltip:Show()
  end)
  charBtn:SetScript("OnLeave", function() GameTooltip:Hide() end)
  f.charBtn = charBtn

  f.charLabel = charBtn:CreateFontString(nil,"OVERLAY","GameFontNormalSmall")
  f.charLabel:SetAllPoints(charBtn)
  f.charLabel:SetTextColor(0.8,0.8,0.5,1)

  f.instanceLabel = f:CreateFontString(nil,"OVERLAY","GameFontNormalSmall")
  f.instanceLabel:SetPoint("RIGHT",bar,"RIGHT",-28,0)
  f.instanceLabel:SetText("")

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

  -- Import button in title bar
  local importBtn = CreateFrame("Button",nil,f)
  importBtn:SetSize(52,18); importBtn:SetPoint("LEFT",bar,"LEFT",130,0)
  importBtn:SetNormalFontObject("GameFontNormalSmall")
  importBtn:SetText("|cff88ffcc[Import]|r")
  importBtn:SetScript("OnClick", function() CreateImportFrame() end)

  -- Meta info line
  f.metaLabel = f:CreateFontString(nil,"OVERLAY","GameFontDisableSmall")
  f.metaLabel:SetPoint("TOPLEFT",f,"TOPLEFT",10,-28)
  f.metaLabel:SetPoint("TOPRIGHT",f,"TOPRIGHT",-10,-28)
  f.metaLabel:SetJustifyH("LEFT")

  -- Tabs
  local TAB_Y = -46
  for i, tabKey in ipairs(TABS) do
    local btn = CreateFrame("Button",nil,f)
    local tabW = (700 - 8) / #TABS  -- will be re-laid out on resize
    btn:SetSize(tabW, 26)
    btn:SetPoint("TOPLEFT",f,"TOPLEFT", 4+(i-1)*tabW, TAB_Y)
    local bg = btn:CreateTexture(nil,"BACKGROUND")
    bg:SetAllPoints(); bg:SetColorTexture(0.10,0.08,0.20,1)
    btn.bg = bg
    local lbl = btn:CreateFontString(nil,"OVERLAY","GameFontNormalSmall")
    lbl:SetAllPoints(); lbl:SetJustifyH("CENTER")
    lbl:SetText(TAB_LABELS[tabKey]); lbl:SetTextColor(0.7,0.7,0.7,1)
    btn.lbl = lbl; btn.tabKey = tabKey
    btn:SetScript("OnClick", function()
      currentTab = tabKey
      settings.lastTab = tabKey
      SaveSettings()
      -- Only reset specific-source filters (string), preserve "show all" (false)
      if type(manualFilter) == "string" then
        manualFilter = nil
      end
      SimWishlist_Refresh()
    end)
    btn:SetScript("OnEnter", function(self)
      if self.tabKey ~= currentTab then self.bg:SetColorTexture(0.18,0.14,0.30,1) end
    end)
    btn:SetScript("OnLeave", function(self)
      if self.tabKey ~= currentTab then self.bg:SetColorTexture(0.10,0.08,0.20,1) end
    end)
    tabButtons[tabKey] = btn
  end

  -- Filter/sort bar
  local filterBar = CreateFrame("Frame",nil,f)
  filterBar:SetPoint("TOPLEFT",f,"TOPLEFT",8, TAB_Y-26-4)
  filterBar:SetPoint("TOPRIGHT",f,"TOPRIGHT",-8, TAB_Y-26-4)
  filterBar:SetHeight(20)
  f.filterBar = filterBar

  f.filterLabel = filterBar:CreateFontString(nil,"OVERLAY","GameFontNormalSmall")
  f.filterLabel:SetPoint("LEFT",filterBar,"LEFT",2,0)
  f.filterLabel:SetPoint("RIGHT",filterBar,"RIGHT",-285,0)
  f.filterLabel:SetJustifyH("LEFT"); f.filterLabel:SetWordWrap(false)

  local function MakeBtn(w, text, onClick)
    local b = CreateFrame("Button",nil,filterBar,"UIPanelButtonTemplate")
    b:SetSize(w,18); b:SetText(text)
    b:SetScript("OnClick", onClick)
    return b
  end

  local showAllBtn = MakeBtn(72,"Show All", function()
    manualFilter = false; SimWishlist_Refresh()
  end)
  showAllBtn:SetPoint("RIGHT",filterBar,"RIGHT",0,0)
  f.showAllBtn = showAllBtn

  local autoBtn = MakeBtn(70,"Use Auto", function()
    manualFilter = nil; SimWishlist_Refresh()
  end)
  autoBtn:SetPoint("RIGHT",showAllBtn,"LEFT",-4,0)
  f.autoBtn = autoBtn

  local minBtn = MakeBtn(72,"Min: 0.0", function()
    local opts = {0, 0.5, 1.0, 2.0}
    for i,v in ipairs(opts) do
      if math.abs(v - (settings.minUpgrade or 0)) < 0.01 then
        settings.minUpgrade = opts[(i % #opts)+1]
        SaveSettings()
        SimWishlist_Refresh()
        return
      end
    end
    settings.minUpgrade = 0; SaveSettings(); SimWishlist_Refresh()
  end)
  minBtn:SetPoint("RIGHT",autoBtn,"LEFT",-4,0)
  f.minBtn = minBtn

  local sortBtn = MakeBtn(88,"Sort: Upgrade", function()
    local modes = {"upgrade","ilvl","name"}
    for i,m in ipairs(modes) do
      if m == settings.sortMode then
        settings.sortMode = modes[(i % #modes)+1]
        SaveSettings(); SimWishlist_Refresh()
        return
      end
    end
    settings.sortMode = "upgrade"; SaveSettings(); SimWishlist_Refresh()
  end)
  sortBtn:SetPoint("RIGHT",minBtn,"LEFT",-4,0)
  f.sortBtn = sortBtn

  -- Divider
  local div = f:CreateTexture(nil,"BACKGROUND")
  div:SetPoint("TOPLEFT",f,"TOPLEFT",4, TAB_Y-26-26)
  div:SetPoint("TOPRIGHT",f,"TOPRIGHT",-4, TAB_Y-26-26)
  div:SetHeight(1); div:SetColorTexture(0.4,0.3,0.6,0.4)

  -- Column header bar
  local colBar = CreateFrame("Frame",nil,f)
  colBar:SetPoint("TOPLEFT",f,"TOPLEFT",8, TAB_Y-26-28)
  colBar:SetPoint("TOPRIGHT",f,"TOPRIGHT",-28, TAB_Y-26-28)
  colBar:SetHeight(18)
  local colBarBg = colBar:CreateTexture(nil,"BACKGROUND")
  colBarBg:SetAllPoints(); colBarBg:SetColorTexture(0.08,0.06,0.18,1)
  f.colBar = colBar

  -- Name column header (left-anchored, fills remaining space)
  local colName = colBar:CreateFontString(nil,"OVERLAY","GameFontNormalSmall")
  colName:SetPoint("LEFT",colBar,"LEFT",30,0)
  colName:SetTextColor(0.7,0.6,0.9,1)
  colName:SetText("Item Name")
  f.colName = colName

  -- Boss column header + drag handle
  local colBossLbl = colBar:CreateFontString(nil,"OVERLAY","GameFontNormalSmall")
  colBossLbl:SetTextColor(0.7,0.6,0.9,1); colBossLbl:SetText("Source")
  colBossLbl:SetJustifyH("RIGHT")
  f.colBossLbl = colBossLbl

  local colIlvlLbl = colBar:CreateFontString(nil,"OVERLAY","GameFontNormalSmall")
  colIlvlLbl:SetTextColor(0.7,0.6,0.9,1); colIlvlLbl:SetText("iLvl")
  colIlvlLbl:SetJustifyH("CENTER")
  f.colIlvlLbl = colIlvlLbl

  local colUpgLbl = colBar:CreateFontString(nil,"OVERLAY","GameFontNormalSmall")
  colUpgLbl:SetTextColor(0.7,0.6,0.9,1); colUpgLbl:SetText("Upgrade")
  colUpgLbl:SetJustifyH("RIGHT")
  f.colUpgLbl = colUpgLbl

  -- Drag handles between columns
  local function MakeDragHandle(parent, cursor, onDrag)
    local h = CreateFrame("Button",nil,parent)
    h:SetSize(6,18); h:SetFrameLevel(parent:GetFrameLevel()+2)
    local tex = h:CreateTexture(nil,"ARTWORK")
    tex:SetAllPoints(); tex:SetColorTexture(0.5,0.3,0.8,0.4)
    h:SetScript("OnEnter", function(self)
      tex:SetColorTexture(0.7,0.5,1,0.8)
      SetCursor(cursor or "Interface\CURSOR\UI-Cursor-SizeLeftRight")
    end)
    h:SetScript("OnLeave", function(self)
      tex:SetColorTexture(0.5,0.3,0.8,0.4)
      ResetCursor()
    end)
    local dragging = false
    local startX = 0
    h:SetScript("OnMouseDown", function(self, btn)
      if btn == "LeftButton" then
        dragging = true
        startX = GetCursorPosition()
        h:SetScript("OnUpdate", function()
          if not dragging then h:SetScript("OnUpdate",nil) return end
          local cx = GetCursorPosition()
          local delta = math.floor((cx - startX) / UIParent:GetEffectiveScale())
          if math.abs(delta) > 0 then
            onDrag(delta)
            startX = cx
          end
        end)
      end
    end)
    h:SetScript("OnMouseUp", function(self, btn)
      dragging = false
      h:SetScript("OnUpdate",nil)
      SaveSettings()
    end)
    return h
  end

  -- Handle between boss/ilvl
  f.dragBossIlvl = MakeDragHandle(colBar, nil, function(delta)
    settings.colBoss = math.max(60, math.min(220, (settings.colBoss or 140) - delta))
    SimWishlist_LayoutColumns()
    SimWishlist_Refresh()
  end)

  -- Handle between ilvl/upgrade
  f.dragIlvlUpg = MakeDragHandle(colBar, nil, function(delta)
    settings.colIlvl = math.max(28, math.min(80, (settings.colIlvl or 36) - delta))
    SimWishlist_LayoutColumns()
    SimWishlist_Refresh()
  end)

  -- Scroll
  local scrollTop = TAB_Y - 26 - 48
  local sf = CreateFrame("ScrollFrame","SimWishlistScroll",f,"UIPanelScrollFrameTemplate")
  sf:SetPoint("TOPLEFT",f,"TOPLEFT",8,scrollTop)
  sf:SetPoint("BOTTOMRIGHT",f,"BOTTOMRIGHT",-28,20)

  local content = CreateFrame("Frame",nil,sf)
  content:SetSize(660,1)
  sf:SetScrollChild(content)
  f.content = content
  f.scrollFrame = sf

  -- Resize grip
  local grip = CreateFrame("Button",nil,f)
  grip:SetSize(16,16)
  grip:SetPoint("BOTTOMRIGHT",f,"BOTTOMRIGHT",0,0)
  grip:SetFrameLevel(f:GetFrameLevel() + 10)
  grip:SetNormalTexture("Interface/ChatFrame/UI-ChatIM-SizeGrabber-Up")
  grip:SetHighlightTexture("Interface/ChatFrame/UI-ChatIM-SizeGrabber-Highlight")
  grip:SetPushedTexture("Interface/ChatFrame/UI-ChatIM-SizeGrabber-Down")
  grip:SetScript("OnMouseDown", function(_,btn)
    if btn == "LeftButton" then f:StartSizing("BOTTOMRIGHT") end
  end)
  grip:SetScript("OnMouseUp", function()
    f:StopMovingOrSizing()
    local w = math.floor(f:GetWidth()+0.5)
    local h = math.floor(f:GetHeight()+0.5)
    settings.windowSize = {w=w, h=h}
    SaveSettings()
    -- Reflow tabs
    local tw = (f:GetWidth()-8)/#TABS
    for i,tabKey in ipairs(TABS) do
      local btn = tabButtons[tabKey]
      if btn then
        btn:ClearAllPoints()
        btn:SetSize(tw,26)
        btn:SetPoint("TOPLEFT",f,"TOPLEFT",4+(i-1)*tw, TAB_Y)
      end
    end
    content:SetWidth(f:GetWidth()-40)
    SimWishlist_LayoutColumns()
    if f:IsShown() then SimWishlist_Refresh() end
  end)

  -- Update layout continuously while resizing
  f:SetScript("OnSizeChanged", function(self)
    local tw = (self:GetWidth()-8)/#TABS
    for i,tabKey in ipairs(TABS) do
      local btn = tabButtons[tabKey]
      if btn then
        btn:ClearAllPoints()
        btn:SetSize(tw,26)
        btn:SetPoint("TOPLEFT",self,"TOPLEFT",4+(i-1)*tw, TAB_Y)
      end
    end
    content:SetWidth(math.max(200, self:GetWidth()-40))
    SimWishlist_LayoutColumns()
  end)

  f:SetScript("OnShow", function(self)
    -- Show What's New on first open after a version update
    SimWishlistCharDB = SimWishlistCharDB or {}
    if SimWishlistCharDB.lastSeenVersion ~= VERSION then
      C_Timer.After(0.5, function() CreateWhatsNewFrame() end)
    end
    -- Defer one frame so the frame has fully rendered before laying out columns
    C_Timer.After(0, function()
      if not self:IsShown() then return end
      content:SetWidth(math.max(200, self:GetWidth()-40))
      SimWishlist_LayoutColumns()
      SimWishlist_Refresh()
    end)
  end)

  return f
end

-- ============================================================
-- Render
-- ============================================================
function SimWishlist_Refresh()
  if not frame or not frame:IsShown() then return end
  local ok, err = pcall(SimWishlist_RefreshInner)
  if not ok then
    print("|cffff4444SimWishlist Refresh ERROR: " .. tostring(err) .. "|r")
  end
end

function SimWishlist_RefreshInner()
  if not frame or not frame:IsShown() then return end

  -- Update char label
  if viewingChar then
    frame.charLabel:SetText(GetViewedCharKey() .. "  |cffff8800(viewing)|r")
    frame.charLabel:SetTextColor(1, 0.6, 0.2, 1)
  else
    frame.charLabel:SetText(GetViewedCharKey())
    frame.charLabel:SetTextColor(0.8, 0.8, 0.5, 1)
  end

  -- Update tab highlights
  for _, tabKey in ipairs(TABS) do
    local btn = tabButtons[tabKey]
    if btn then
      if tabKey == currentTab then
        btn.bg:SetColorTexture(0.25,0.15,0.45,1)
        btn.lbl:SetTextColor(1,0.85,1,1)
      else
        btn.bg:SetColorTexture(0.10,0.08,0.20,1)
        btn.lbl:SetTextColor(0.7,0.7,0.7,1)
      end
    end
  end

  -- Instance detection + auto-filter
  local instName, instType = GetCurrentInstance()
  if instName then
    frame.instanceLabel:SetText("|cff88ff88📍 "..instName.."|r")
    -- Auto-switch tab
    if manualFilter == nil and instType then
      currentTab = instType
    end
  else
    frame.instanceLabel:SetText("")
  end

  -- Filter label
  local autoSrc = instName
  if manualFilter == false then
    frame.filterLabel:SetText("|cffaaaaaa Showing all sources|r")
  elseif type(manualFilter) == "string" then
    frame.filterLabel:SetText("|cffcc88ff Filter: |r|cffffff88"..manualFilter.."|r")
  elseif autoSrc then
    frame.filterLabel:SetText("|cff88ff88 Auto: |r|cffffff88"..autoSrc.."|r")
  else
    frame.filterLabel:SetText("|cffaaaaaa Showing all sources|r")
  end

  -- Buttons
  local smode = settings.sortMode or "upgrade"
  frame.sortBtn:SetText("Sort: "..(smode:sub(1,1):upper()..smode:sub(2)))
  frame.minBtn:SetText(string.format("Min: %.1f", settings.minUpgrade or 0))

  -- Meta label
  local data = GetViewedCharData()
  ComputeBiSMap()
  if data.importTime and data.importTime > 0 then
    frame.metaLabel:SetText(
      "|cff88c0ff"..(data.source or "Unknown").."|r  •  "..
      "Items: |cffffff88"..(data.items and #data.items or 0).."|r  •  "..
      "Imported: |cffaaaaaa"..date("%Y-%m-%d %H:%M", data.importTime).."|r"
    )
  else
    frame.metaLabel:SetText("|cffff8888No data. Click [Import] to get started.|r")
  end

  -- Get and filter items
  local items = GetItemsForTab(currentTab)

  -- Apply source filter
  local srcFilter = nil
  if type(manualFilter) == "string" then
    srcFilter = manualFilter
  elseif manualFilter == nil and instName then
    srcFilter = instName
  end

  if currentTab ~= "BY SLOT" then
    items = FilterBySource(items, srcFilter)
  end
  items = FilterByMinUpgrade(items)
  items = FilterOutTierSets(items)
  SortItems(items)

  -- Group
  local groups
  if currentTab == "BY SLOT" then
    groups = GroupBySlot(items)
  else
    groups = GroupBySource(items)
  end

  HidePool(rowPool)
  HidePool(hdrPool)

  if #groups == 0 or (#groups == 1 and #groups[1].items == 0) then
    local h = GetHeader(1, frame.content)
    h:SetPoint("TOPLEFT",frame.content,"TOPLEFT",0,0)
    h:SetWidth(frame.content:GetWidth())
    h.lbl:SetText("|cffaaaaaa No items. Try 'Show All' or import data.|r")
    h.filterBtn:Hide()
    h:Show()
    frame.content:SetHeight(24)
    return
  end

  local yOff  = -PAD
  local rowI  = 0
  local hdrI  = 0
  local cW    = frame.content:GetWidth()

  -- Sync content width to frame, then apply column layout
  frame.content:SetWidth(math.max(200, frame:GetWidth() - 40))
  SimWishlist_LayoutColumns()

  for _, group in ipairs(groups) do
    if #group.items > 0 then
      local colKey = currentTab .. "|" .. group.source
      local isCollapsed = collapsedGroups[colKey]

      hdrI = hdrI + 1
      local hdr = GetHeader(hdrI, frame.content)
      yOff = yOff - HDR_GAP
      hdr:SetPoint("TOPLEFT",frame.content,"TOPLEFT",0,yOff)
      hdr:SetWidth(cW)
      hdr.collapseKey = colKey
      hdr.arrow:SetText(isCollapsed and "+" or "-")
      hdr.lbl:SetText(group.source.."  |cff888888("..#group.items..")|r")
      hdr.filterBtn:Show()
      local srcCapture = group.source
      hdr.filterBtn:SetScript("OnClick", function(self)
        manualFilter = srcCapture
        SimWishlist_Refresh()
      end)
      hdr:Show()
      yOff = yOff - HDR_H - PAD

      if not isCollapsed then
        if currentTab == "RAID" then
          -- Boss sub-headers for raids
          local bossGroups = GroupByBoss(group.items)
          for _, bg in ipairs(bossGroups) do
            if #bg.items > 0 then
              local bossColKey = colKey .. "|" .. bg.boss
              local bossCollapsed = collapsedGroups[bossColKey]
              hdrI = hdrI + 1
              local bossHdr = GetHeader(hdrI, frame.content)
              yOff = yOff - 2
              bossHdr:SetPoint("TOPLEFT",frame.content,"TOPLEFT",10,yOff)
              bossHdr:SetWidth(cW - 10)
              bossHdr.collapseKey = bossColKey
              bossHdr.arrow:SetText(bossCollapsed and "+" or "-")
              bossHdr.lbl:SetText("|cffaaaaaa"..bg.boss.."|r  |cff666666("..#bg.items..")|r")
              bossHdr.filterBtn:Hide()
              bossHdr:Show()
              yOff = yOff - HDR_H - PAD
              if not bossCollapsed then
                for bi, item in ipairs(bg.items) do
                  rowI = rowI + 1
                  local row = GetRow(rowI, frame.content)
                  row:SetPoint("TOPLEFT",frame.content,"TOPLEFT",10,yOff)
                  row:SetWidth(cW - 10)
                  row.itemData = item
                  row.altRow   = (bi % 2 == 0)
                  local name = GetItemInfo(item.itemID) or ("Item #"..tostring(item.itemID))
                  if IsBiS(item.itemID) then name = name .. BIS_TAG end
                  row.nameLbl:SetText(name)
                  local _, _, _, _, iconTex = C_Item.GetItemInfoInstant(item.itemID)
                  if iconTex then row.icon:SetTexture(iconTex); row.icon:Show() else row.icon:Hide() end
                  row.bossLbl:SetText("")
                  row.ilvlLbl:SetText(item.ilvl and item.ilvl > 0 and ("|cff999999"..item.ilvl.."|r") or "")
                  row.upgLbl:SetText(FmtUpgrade(item.upgrade))
                  row.bg:SetColorTexture(row.altRow and 0.06 or 0, row.altRow and 0.05 or 0, row.altRow and 0.12 or 0, row.altRow and 0.4 or 0)
                  row:Show()
                  yOff = yOff - ROW_H
                end -- for bi
                yOff = yOff - PAD
              end -- if not bossCollapsed
            end -- if #bg.items
          end -- for bossGroups
        else
          -- Dungeon and other tabs: flat list
          for i, item in ipairs(group.items) do
            rowI = rowI + 1
            local row = GetRow(rowI, frame.content)
            row:SetPoint("TOPLEFT",frame.content,"TOPLEFT",0,yOff)
            row:SetWidth(cW)
            row.itemData = item
            row.altRow   = (i % 2 == 0)
            local loot = GetLootInfo(item.itemID)
            local name = GetItemInfo(item.itemID) or ("Item #"..tostring(item.itemID))
            if IsBiS(item.itemID) then name = name .. BIS_TAG end
            row.nameLbl:SetText(name)
            local _, _, _, _, iconTex = C_Item.GetItemInfoInstant(item.itemID)
            if iconTex then row.icon:SetTexture(iconTex); row.icon:Show() else row.icon:Hide() end
            local sourceLabel
            local loc = (item.dropLoc or ""):upper()
            if currentTab == "CRAFTED" or loc == "CRAFTED" then
              sourceLabel = "Crafting"
            elseif currentTab == "DELVES" or loc == "DELVES" then
              sourceLabel = "Delves"
            else
              sourceLabel = (loot and loot.boss) or ""
            end
            row.bossLbl:SetText(sourceLabel ~= "" and ("|cff888888"..sourceLabel.."|r") or "")
            row.ilvlLbl:SetText(item.ilvl and item.ilvl > 0 and ("|cff999999"..item.ilvl.."|r") or "")
            row.upgLbl:SetText(FmtUpgrade(item.upgrade))
            row.bg:SetColorTexture(row.altRow and 0.06 or 0, row.altRow and 0.05 or 0, row.altRow and 0.12 or 0, row.altRow and 0.4 or 0)
            row:Show()
            yOff = yOff - ROW_H
          end -- for group.items
          yOff = yOff - PAD
        end -- if RAID
      end -- if not isCollapsed
    end -- if #group.items
  end -- for groups

  frame.content:SetHeight(math.abs(yOff) + 8)
end

-- ============================================================
-- Init & Events
-- ============================================================
function SlashHandler(msg)
  msg = strtrim((msg or ""):lower())
  if not frame then SimWishlist_Init() end
  if not frame then return end

  if msg == "import" then
    CreateImportFrame()
    return
  elseif msg == "news" or msg == "changelog" then
    CreateWhatsNewFrame()
    return
  elseif msg == "all" then
    manualFilter = false
    if frame:IsShown() then SimWishlist_Refresh() end
    return
  elseif msg == "auto" then
    manualFilter = nil
    local instName, instType = GetCurrentInstance()
    if instType then currentTab = instType end
    frame:Show()
    SimWishlist_Refresh()
    return
  elseif msg == "test" then
    local instName, instType = GetCurrentInstance()
    if instType then currentTab = instType end
    manualFilter = nil
    frame:Show()
    SimWishlist_Refresh()
    print("|cffcc88ff{{ Sim Wishlist }}|r |cffffff00[TEST]|r Simulating M+ completion.")
    return
  elseif msg == "help" then
    print("|cffcc88ff{{ Sim Wishlist }}|r |cffffff00/swl|r commands:")
    print("  |cffffff00/swl|r          - Toggle window")
    print("  |cffffff00/swl import|r   - Open import box")
    print("  |cffffff00/swl all|r      - Show all items regardless of location")
    print("  |cffffff00/swl auto|r     - Open filtered to current instance")
    print("  |cffffff00/swl test|r     - Simulate M+ completion (for testing)")
    print("  |cffffff00/swl news|r     - Show What's New changelog")
    print("  |cffffff00/swl help|r     - Show this message")
    return
  end

  if frame:IsShown() then
    frame:Hide()
  else
    local instName, instType = GetCurrentInstance()
    if instType then currentTab = instType end
    manualFilter = nil
    frame:Show()
    SimWishlist_Refresh()
  end
end

-- ============================================================
-- What's New changelog popup
-- ============================================================
CreateWhatsNewFrame = function()
  if whatsNewFrame then
    whatsNewFrame:Show()
    return
  end

  local f = CreateFrame("Frame", "SimWishlistWhatsNew", UIParent, "BackdropTemplate")
  f:SetSize(460, 420)
  f:SetPoint("CENTER")
  f:SetMovable(true)
  f:EnableMouse(true)
  f:RegisterForDrag("LeftButton")
  f:SetScript("OnDragStart", f.StartMoving)
  f:SetScript("OnDragStop", f.StopMovingOrSizing)
  f:SetClampedToScreen(true)
  f:SetFrameStrata("DIALOG")
  f:SetFrameLevel(200)

  f:SetBackdrop({
    bgFile="Interface/Tooltips/UI-Tooltip-Background",
    edgeFile="Interface/Tooltips/UI-Tooltip-Border",
    tile=true, tileSize=16, edgeSize=16,
    insets={left=4,right=4,top=4,bottom=4}
  })
  f:SetBackdropColor(0.06, 0.05, 0.14, 0.98)
  f:SetBackdropBorderColor(0.5, 0.35, 0.8, 1)

  -- Mark as seen when hidden (X button, Got it, escape, anything)
  f:SetScript("OnHide", function()
    SimWishlistCharDB = SimWishlistCharDB or {}
    SimWishlistCharDB.lastSeenVersion = VERSION
  end)

  local close = CreateFrame("Button",nil,f,"UIPanelCloseButton")
  close:SetPoint("TOPRIGHT",f,"TOPRIGHT",2,2)
  close:SetScript("OnClick", function() f:Hide() end)

  local title = f:CreateFontString(nil,"OVERLAY","GameFontNormalLarge")
  title:SetPoint("TOP", f, "TOP", 0, -16)
  title:SetText("|cffcc88ffSim Wishlist — What's New|r")

  -- Scrollable content area
  local sf = CreateFrame("ScrollFrame", nil, f, "UIPanelScrollFrameTemplate")
  sf:SetPoint("TOPLEFT", f, "TOPLEFT", 12, -44)
  sf:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -30, 52)

  local content = CreateFrame("Frame", nil, sf)
  content:SetWidth(400)
  sf:SetScrollChild(content)

  -- Build all labels, store them for deferred layout
  local elements = {}
  for idx, entry in ipairs(CHANGELOG) do
    local isLatest = (idx == 1)

    -- Version header
    local vLbl = content:CreateFontString(nil, "OVERLAY",
      isLatest and "GameFontNormalLarge" or "GameFontNormal")
    vLbl:SetWidth(390)
    vLbl:SetJustifyH("LEFT")
    if isLatest then
      vLbl:SetText("|cffcc88ffv" .. entry.version .. "|r  |cff44ff88NEW|r  |cff666666" .. (entry.date or "") .. "|r")
    else
      vLbl:SetText("|cffaa88ccv" .. entry.version .. "|r  |cff555555" .. (entry.date or "") .. "|r")
    end
    elements[#elements+1] = { lbl=vLbl, kind="header", isLatest=isLatest }

    -- Bullet entries
    for _, text in ipairs(entry.entries) do
      local bLbl = content:CreateFontString(nil, "OVERLAY",
        isLatest and "GameFontNormal" or "GameFontNormalSmall")
      bLbl:SetWidth(370)
      bLbl:SetJustifyH("LEFT")
      bLbl:SetWordWrap(true)
      bLbl:SetSpacing(3)
      if isLatest then
        bLbl:SetText("|cffffffcc*  " .. text .. "|r")
      else
        bLbl:SetText("|cff888888*  " .. text .. "|r")
      end
      elements[#elements+1] = { lbl=bLbl, kind="bullet", isLatest=isLatest }
    end

    -- Version gap marker
    elements[#elements+1] = { kind="gap" }
  end

  -- Layout function — positions all elements using real heights
  local function LayoutChangelog()
    local y = -8
    for _, el in ipairs(elements) do
      if el.kind == "gap" then
        y = y - 20
      elseif el.kind == "header" then
        el.lbl:ClearAllPoints()
        el.lbl:SetPoint("TOPLEFT", content, "TOPLEFT", 4, y)
        local h = el.lbl:GetStringHeight()
        if h < 8 then h = el.isLatest and 18 or 14 end
        y = y - h - (el.isLatest and 14 or 12)
      else -- bullet
        el.lbl:ClearAllPoints()
        el.lbl:SetPoint("TOPLEFT", content, "TOPLEFT", 18, y)
        local h = el.lbl:GetStringHeight()
        if h < 8 then h = el.isLatest and 14 or 12 end
        y = y - h - 10
      end
    end
    content:SetHeight(math.abs(y) + 16)
  end

  -- Initial layout (heights may be 0 — fallbacks handle it)
  LayoutChangelog()

  -- Re-layout after one frame so GetStringHeight returns real values
  f:SetScript("OnShow", function()
    SimWishlistCharDB = SimWishlistCharDB or {}
    SimWishlistCharDB.lastSeenVersion = VERSION
    C_Timer.After(0, LayoutChangelog)
  end)

  -- Got it button
  local gotItBtn = CreateFrame("Button", nil, f, "UIPanelButtonTemplate")
  gotItBtn:SetSize(120, 28)
  gotItBtn:SetPoint("BOTTOM", f, "BOTTOM", 0, 14)
  gotItBtn:SetText("Got it!")
  gotItBtn:SetScript("OnClick", function() f:Hide() end)

  whatsNewFrame = f
  f:Show()
end

function SimWishlist_Init()
  if frame then return end
  LoadSettings()
  SimWishlistDB = SimWishlistDB or {}
  currentTab = settings.lastTab or "DUNGEON"
  frame = CreateMainFrame()
  SLASH_SWL1 = "/swl"
  SLASH_SWL2 = "/simwishlist"
  SlashCmdList["SWL"] = SlashHandler

  C_Timer.After(1, function()
    print("|cffcc88ff{{Sim Wishlist}}|r|cffffffff v"..VERSION.."|r Loaded.")
    print("|cffcc88ff{{Sim Wishlist}}|r|cffffff00 /swl|r to open. |cffffff00/swl help|r for commands. |cffffff00/swl news|r for changelog.")
  end)
end


-- Direct init call with error surfacing
local _initOk, _initErr = pcall(SimWishlist_Init)
if not _initOk then
  print("|cffff4444SimWishlist INIT ERROR: " .. tostring(_initErr) .. "|r")
end



-- Zone change and M+ completion handling
local evFrame = CreateFrame("Frame")
evFrame:RegisterEvent("PLAYER_ENTERING_WORLD")
evFrame:RegisterEvent("ZONE_CHANGED_NEW_AREA")
evFrame:RegisterEvent("GET_ITEM_INFO_RECEIVED")
evFrame:RegisterEvent("CHALLENGE_MODE_COMPLETED")
evFrame:SetScript("OnEvent", function(_, event)
  if event == "GET_ITEM_INFO_RECEIVED" then
    if frame and frame:IsShown() then SimWishlist_Refresh() end
    return
  end
  if event == "PLAYER_ENTERING_WORLD" or event == "ZONE_CHANGED_NEW_AREA" then
    if frame and frame:IsShown() then
      manualFilter = nil
      SimWishlist_Refresh()
    end
    return
  end
  if event == "CHALLENGE_MODE_COMPLETED" then
    -- Auto-open after M+ completion, filtered to current dungeon
    if not frame then SimWishlist_Init() end
    C_Timer.After(3, function()  -- slight delay so the end-of-run UI settles
      if not frame then return end
      local instName, instType = GetCurrentInstance()
      if instType then currentTab = instType end
      manualFilter = nil
      frame:Show()
      SimWishlist_Refresh()
      print("|cffcc88ff{{Sim Wishlist}}|r |cff44ff88Key complete! Showing upgrades for this dungeon.|r")
    end)
    return
  end
end)