-- Loot Wishlist - Core and SavedVariables

local ADDON_NAME = ... or "LootWishlist"

-- (Legacy migration removed)

-- New per-character DB
LootWishlistCharDB = LootWishlistCharDB or {}
-- New account-wide DB
LootWishlistDB = LootWishlistDB or {}

local DEBUG = false

-- Dev logging via LuckyLog
local DevLog  = LuckyLog:New("[Lwl][debug]", function() return DEBUG end)
local PerfLog = LuckyLog:New("|cffff8800[LWL-perf]|r", function() return DEBUG end)

-- Public API table
LootWishlist = {
  DEBUG = function() return DEBUG end,
}

-- Local state
local trackedItems -- assigned after DB init; may use string keys for difficulty variants

-- No basic frame: custom raw-frame UI is the only path now

-- Spec detection ------------------------------------------------------------
local function tryGetItemSpecsImmediate(itemID, itemLink)
  local query = itemLink or (itemID and ("item:"..tostring(itemID))) or itemID
  if not query then return nil end
  -- Prefer modern API
  if C_Item and C_Item.GetItemSpecInfo then
    local ok, res = pcall(C_Item.GetItemSpecInfo, query)
    if ok and type(res) == "table" and #res > 0 then return res end
  end
  return nil
end

local function computeItemSpecs(itemID, itemLink, onDone)
  -- Try immediately first
  local specs = tryGetItemSpecsImmediate(itemID, itemLink)
  if specs then if onDone then onDone(specs) end; return end
  -- Try async load via Item API if available
  local ItemAPI = _G and _G["Item"]
  if type(ItemAPI) == "table" and ItemAPI.CreateFromItemID then
    local obj = ItemAPI:CreateFromItemID(itemID)
    if obj and obj.ContinueOnItemLoad then
      obj:ContinueOnItemLoad(function()
        local again = tryGetItemSpecsImmediate(itemID, (obj.GetItemLink and obj:GetItemLink()) or itemLink)
        if onDone then onDone(again or {}) end
      end)
      return
    end
  end
  -- Fallback: request and call back empty
  if C_Item and C_Item.RequestLoadItemDataByID then C_Item.RequestLoadItemDataByID(itemID) end
  if onDone then onDone({}) end
end

function LootWishlist.GetSpecNames(specIDs)
  local names = {}
  if type(specIDs) ~= "table" then return names end
  local getInfo = _G and _G["GetSpecializationInfoByID"]
  for _, sid in ipairs(specIDs) do
    if type(getInfo) == "function" then
      local ok, _specID, specName = pcall(getInfo, sid)
      if ok and type(specName) == "string" and specName ~= "" then
        table.insert(names, specName)
      end
    end
  end
  return names
end

-- DB and migration -----------------------------------------------------------
local function InitializeDB()
  -- Initialize per-character DB
  LootWishlistCharDB.trackedItems = LootWishlistCharDB.trackedItems or {}
  LootWishlist.trackedItems = LootWishlistCharDB.trackedItems
  trackedItems = LootWishlist.trackedItems

  -- Initialize settings tables
  LootWishlistCharDB.settings = LootWishlistCharDB.settings or {}
  LootWishlistDB.settings = LootWishlistDB.settings or {}

  local C = LootWishlist.Const or {}
  local charS = LootWishlistCharDB.settings
  local acctS = LootWishlistDB.settings

  -- Migration: move whisper/party templates from per-character to account-wide if present
  if charS.whisperTemplate and not acctS.whisperTemplate then acctS.whisperTemplate = charS.whisperTemplate; charS.whisperTemplate = nil end
  if charS.partyTemplate and not acctS.partyTemplate then acctS.partyTemplate = charS.partyTemplate; charS.partyTemplate = nil end

  -- Migration: move raid roll alert toggle from per-character to account-wide
  if charS.enableRaidRollAlert ~= nil and acctS.enableRaidRollAlert == nil then
    acctS.enableRaidRollAlert = charS.enableRaidRollAlert
    charS.enableRaidRollAlert = nil
  end

  -- Defaults for account-wide templates
  acctS.whisperTemplate = acctS.whisperTemplate or C.DEFAULT_WHISPER_TEMPLATE 
  acctS.partyTemplate = acctS.partyTemplate or C.DEFAULT_PARTY_TEMPLATE
  if acctS.debug == nil then acctS.debug = false end
  DEBUG = acctS.debug and true or false

  -- Account-wide toggle default
  if acctS.enableRaidRollAlert == nil then acctS.enableRaidRollAlert = true end
  if acctS.hideSummaryWindow == nil then acctS.hideSummaryWindow = false end
  if acctS.bossKillReminderDelay == nil then acctS.bossKillReminderDelay = 10 end

  -- Restore window position is handled by Ace frame status table

  -- Gentle migration for legacy tracked shapes (pre-difficulty and non-table values)
  do
    local toAdd = {}
    local toRemove = {}
    local function parseKeyParts(k)
      if k == nil then return nil, nil end
      local ks = tostring(k)
  local id = tonumber(ks:match("^(%d+)$") or ks:match("^(%d+)%@%d+$"))
  local diff = tonumber(ks:match("^%d+%@(%d+)$"))
      return id, diff
    end
    for k, v in pairs(trackedItems) do
      if type(v) ~= "table" or (type(v)=="table" and type(v.id) ~= "number") then
        local id, diff = parseKeyParts(k)
        if id then
          local newKey = diff and (tostring(id).."@"..tostring(diff)) or tostring(id)
          toAdd[newKey] = toAdd[newKey] or {
            id = id,
            boss = nil,
            dungeon = nil,
            isRaid = nil,
            link = nil,
            encounterID = nil,
            instanceID = nil,
            difficultyID = diff,
            difficultyName = nil,
            specs = nil,
          }
          table.insert(toRemove, k)
        end
      end
    end
    for k in pairs(toAdd) do
      trackedItems[k] = trackedItems[k] or toAdd[k]
    end
    for _, k in ipairs(toRemove) do
      trackedItems[k] = nil
    end
  end

  -- Backfill specs for existing tracked entries if missing.
  -- Callbacks use deferredRefresh to avoid a per-item cascade of full rebuilds.
  for _, v in pairs(trackedItems) do
    if type(v) == "table" and type(v.id) == "number" and (v.specs == nil or (type(v.specs)=="table" and next(v.specs)==nil)) then
      computeItemSpecs(v.id, v.link, function(specs)
        v.specs = specs or {}
        local ace = LootWishlist.Ace
        if ace and ace.deferredRefresh then ace.deferredRefresh()
        elseif ace and ace.refresh then ace.refresh() end
        if LootWishlist.Summary and LootWishlist.Summary.refresh then LootWishlist.Summary.refresh() end
      end)
    end
  end
end

-- Public API: Add/Remove/Iterate --------------------------------------------
function LootWishlist.AddTrackedItem(itemID, bossName, instanceName, isRaid, itemLink, encounterID, instanceID, difficultyID, difficultyName)
  local t0 = debugprofilestop()
  -- Compose a unique key so that the same item can be tracked for multiple difficulties
  local key
  if difficultyID then
    key = tostring(itemID).."@"..tostring(difficultyID)
  else
    -- Back-compat when difficulty isn\'t provided
    key = tostring(itemID)
  end
  local count = 0
  for _ in pairs(trackedItems) do count = count + 1 end
  PerfLog("AddTrackedItem START id=" .. tostring(itemID) .. " key=" .. key .. " existing=" .. count)
  trackedItems[key] = {
    id = itemID,
    boss = bossName,
    dungeon = instanceName,
    isRaid = isRaid and true or false,
    link = itemLink,
    encounterID = encounterID,
    instanceID = instanceID,
    difficultyID = difficultyID,
    difficultyName = difficultyName,
    specs = nil,
  }
  DevLog("AddTrackedItem id=", itemID, "key=", key, "diffID=", tostring(difficultyID or "nil"), "boss=", tostring(bossName or ""), "instance=", tostring(instanceName or ""))
  -- Compute and attach spec list asynchronously.
  -- Uses deferredRefresh since open() already does an immediate refresh.
  computeItemSpecs(itemID, itemLink, function(specs)
    local entry = trackedItems[key]
    if entry then entry.specs = specs or {} end
    PerfLog("computeItemSpecs CALLBACK for id=" .. tostring(itemID))
    local ace = LootWishlist.Ace
    if ace and ace.deferredRefresh then ace.deferredRefresh()
    elseif ace and ace.refresh then ace.refresh() end
    if LootWishlist.Summary and LootWishlist.Summary.refresh then LootWishlist.Summary.refresh() end
  end)
  -- Prefer Ace view opening; the UI module will handle opening and refreshing
  local tBeforeOpen = debugprofilestop()
  if LootWishlist.Ace and LootWishlist.Ace.open then
    LootWishlist.Ace.open()
    PerfLog("open() took " .. string.format("%.1f", debugprofilestop() - tBeforeOpen) .. "ms")
    if LootWishlist.Summary and LootWishlist.Summary.refresh then LootWishlist.Summary.refresh() end
  else
    print("Loot Wishlist: UI module not loaded. Item tracked.")
  end
  PerfLog("AddTrackedItem END total=" .. string.format("%.1f", debugprofilestop() - t0) .. "ms")
end

-- Remove one or more tracked entries.
-- If keyOrID is a string key, remove that exact entry.
-- If it\'s a number itemID and difficultyID is provided, remove matching entries for that difficulty only.
-- If it\'s a number itemID and difficultyID is nil, remove all entries for that itemID.
function LootWishlist.RemoveTrackedItem(keyOrID, difficultyID)
  -- Treat 0 as no difficulty filter (happens outside instances)
  if type(difficultyID) == "number" and difficultyID <= 0 then difficultyID = nil end
  DevLog("RemoveTrackedItem arg=", tostring(keyOrID), "diffID=", tostring(difficultyID or "nil"))
  local removed = false
  local removedKeys = {}
  local function parseKeyParts(k)
    if k == nil then return nil, nil end
    local ks = tostring(k)
  local id = tonumber(ks:match("^(%d+)$") or ks:match("^(%d+)%@%d+$"))
  local diff = tonumber(ks:match("^%d+%@(%d+)$"))
    return id, diff
  end
  if type(keyOrID) == "string" then
    if trackedItems[keyOrID] then trackedItems[keyOrID] = nil; removed = true; table.insert(removedKeys, tostring(keyOrID)) end
  elseif type(keyOrID) == "number" then
    for k, v in pairs(trackedItems) do
      local vid, vdiff
      if type(v) == "table" then
        vid = v.id
        vdiff = v.difficultyID
      else
        vid, vdiff = parseKeyParts(k)
      end
      local match = (vid == keyOrID and (difficultyID == nil or vdiff == difficultyID or vdiff == nil))
      DevLog("consider key=", tostring(k), "vid=", tostring(vid), "vdiff=", tostring(vdiff), "match=", tostring(match))
      if match then
        trackedItems[k] = nil
        removed = true
        table.insert(removedKeys, tostring(k))
      end
    end
  end
  if removed then
    if DEBUG then
      local cnt = 0
      for _ in pairs(trackedItems) do cnt = cnt + 1 end
      DevLog("removed keys:", table.concat(removedKeys, ", "), "remaining=", cnt)
    end
    if LootWishlist.Ace and LootWishlist.Ace.refresh then LootWishlist.Ace.refresh() end
    if LootWishlist.Summary and LootWishlist.Summary.refresh then LootWishlist.Summary.refresh() end
  else
    DevLog("RemoveTrackedItem: no matching entries removed")
  end
end

function LootWishlist.IterateTracked()
  return pairs(trackedItems)
end

function LootWishlist.GetTracked()
  return trackedItems
end

function LootWishlist.ClearAllTracked()
  if not trackedItems or not next(trackedItems) then return end
  for k in pairs(trackedItems) do trackedItems[k] = nil end
  if LootWishlist.Ace and LootWishlist.Ace.refresh then LootWishlist.Ace.refresh() end
  if LootWishlist.Summary and LootWishlist.Summary.refresh then LootWishlist.Summary.refresh() end
end

do
  -- Proxy that reads/writes account-wide templates and per-character toggles
  local settingsProxy
  function LootWishlist.GetSettings()
    if settingsProxy then return settingsProxy end
    local function buildProxy()
      local proxy = {}
      return setmetatable(proxy, {
        __index = function(_, k)
          local acct = (LootWishlistDB and LootWishlistDB.settings) or {}
          local ch = (LootWishlistCharDB and LootWishlistCharDB.settings) or {}
          if k == "whisperTemplate" or k == "partyTemplate" then
            return acct[k]
          elseif k == "enableRaidRollAlert" then
            return acct[k]
          end
          return acct[k] or ch[k]
        end,
        __newindex = function(_, k, v)
          local acct = (LootWishlistDB and LootWishlistDB.settings) or {}
          local ch = (LootWishlistCharDB and LootWishlistCharDB.settings) or {}
          if k == "whisperTemplate" or k == "partyTemplate" then
            acct[k] = v
          elseif k == "enableRaidRollAlert" then
            acct[k] = v
          else
            acct[k] = v
          end
        end,
      })
    end
    settingsProxy = buildProxy()
    return settingsProxy
  end
end

function LootWishlist.SetDebug(val)
  DEBUG = not not val
  if LootWishlistDB and LootWishlistDB.settings then
    LootWishlistDB.settings.debug = DEBUG
  end
  print("Loot Wishlist debug:", DEBUG and "ON" or "OFF")
end

function LootWishlist.IsDebug()
  return DEBUG
end

-- Events ---------------------------------------------------------------------
local f = CreateFrame("Frame")
f:RegisterEvent("ADDON_LOADED")
f:RegisterEvent("PLAYER_LOGIN")
f:RegisterEvent("GET_ITEM_INFO_RECEIVED")
f:SetScript("OnEvent", function(self, event, ...)
  if event == "ADDON_LOADED" then
    local addonName = ...
    if addonName == ADDON_NAME then
      InitializeDB()
      -- Defer UI refresh to modules after they initialize
    elseif addonName == "Blizzard_EncounterJournal" then
      if LootWishlist.EJ and LootWishlist.EJ.hook then LootWishlist.EJ.hook() end
    end
  elseif event == "PLAYER_LOGIN" then
    print("Loot Wishlist loaded. Track loot from the Adventure Guide! Use /wishlist for help.")
    -- Minimap button
    if LuckyMinimap and LootWishlistDB then
      LootWishlist.minimapButton = LuckyMinimap:Create({
        name    = "LootWishlistMinimapButton",
        icon    = "Interface\\Icons\\INV_Misc_Spyglass_03",
        dbKey   = "minimap",
        db      = LootWishlistDB,
        onClick = function(_, mouseBtn)
          if mouseBtn == "LeftButton" then
            if LootWishlist.Ace and LootWishlist.Ace.open then
              if LootWishlist.Ace.isOpen then
                LootWishlist.Ace.hide()
              else
                LootWishlist.Ace.open()
              end
            end
          elseif mouseBtn == "RightButton" then
            if LootWishlist.Options and LootWishlist.Options.Open then
              LootWishlist.Options.Open()
            end
          elseif mouseBtn == "MiddleButton" then
            LootWishlist.SetDebug(not LootWishlist.IsDebug())
          end
        end,
        tooltip = function(tt)
          tt:AddLine("|cffffd100Lucky's Loot Wishlist|r")
          tt:AddLine(" ")
          tt:AddLine("Left-click: Toggle wishlist", 0.91, 0.86, 0.78)
          tt:AddLine("Right-click: Open settings", 0.91, 0.86, 0.78)
          tt:AddLine("Middle-click: Toggle dev mode", 0.54, 0.49, 0.42)
          tt:AddLine("Shift+drag: Move button", 0.54, 0.49, 0.42)
        end,
      })
    end
    local isLoaded = false
    if C_AddOns and C_AddOns.IsAddOnLoaded then
      isLoaded = C_AddOns.IsAddOnLoaded("Blizzard_EncounterJournal")
    end
    if isLoaded then
      if LootWishlist.EJ and LootWishlist.EJ.hook then LootWishlist.EJ.hook() end
    else
      local loader = CreateFrame("Frame")
      loader:RegisterEvent("ADDON_LOADED")
      loader:SetScript("OnEvent", function(_, _, addonName)
        if addonName == "Blizzard_EncounterJournal" then
          if LootWishlist.EJ and LootWishlist.EJ.hook then LootWishlist.EJ.hook() end
        end
      end)
    end
    -- Initial UI refresh without forcing windows open
    if LootWishlist.Ace and LootWishlist.Ace.refresh then LootWishlist.Ace.refresh() end
    if LootWishlist.Summary and LootWishlist.Summary.showIfNeeded then LootWishlist.Summary.showIfNeeded() end
  elseif event == "GET_ITEM_INFO_RECEIVED" then
    -- When item info is received, try to resolve missing spec info for that item.
    -- Uses deferredRefresh to batch multiple rapid callbacks into one rebuild.
    local itemID = ...
    if type(itemID) == "number" then
      for _, v in pairs(trackedItems) do
        if type(v) == "table" and v.id == itemID and (v.specs == nil or (type(v.specs)=="table" and next(v.specs)==nil)) then
          computeItemSpecs(itemID, v.link, function(specs)
            v.specs = specs or {}
            local ace = LootWishlist.Ace
            if ace and ace.deferredRefresh then ace.deferredRefresh()
            elseif ace and ace.refresh then ace.refresh() end
            if LootWishlist.Summary and LootWishlist.Summary.refresh then LootWishlist.Summary.refresh() end
          end)
        end
      end
    end
  end
end)

-- Slash commands -------------------------------------------------------------
SLASH_WISHLIST1 = "/wishlist"
SLASH_WISHLIST2 = "/lwl" -- short alias for Loot Wishlist
SlashCmdList.WISHLIST = function(msg)
  msg = msg and msg:lower() or ""
  if msg == "show" then
    if LootWishlist.Ace and LootWishlist.Ace.open then LootWishlist.Ace.open() else print("Loot Wishlist: UI module not loaded.") end
  elseif msg == "hide" then
    if LootWishlist.Ace and LootWishlist.Ace.hide then LootWishlist.Ace.hide() end
  elseif msg == "debug" then
    LootWishlist.SetDebug(not LootWishlist.IsDebug())
    if EncounterJournal and EncounterJournal:IsShown() then
      local upd = _G["EncounterJournal_LootUpdate"]
      if type(upd) == "function" then upd() end
    end
  elseif msg:match("^remove ") then
    local idStr = msg:match("^remove%s+(%d+)") or msg:match("item:(%d+)")
    local itemID = idStr and tonumber(idStr)
    if itemID and trackedItems[itemID] then
      LootWishlist.RemoveTrackedItem(itemID)
      print("Loot Wishlist: removed", itemID)
    else
      print("Loot Wishlist: item not tracked; usage /wishlist remove <itemID>")
    end
  elseif msg == "list" then
    local n = 0
    for _ in pairs(trackedItems) do n = n + 1 end
    print("Loot Wishlist: tracking", n, "item(s)")
  elseif msg == "options" or msg == "settings" then
    if LootWishlist.Options and LootWishlist.Options.Open then
      LootWishlist.Options.Open()
    else
      print("Open Interface Options and look for 'Loot Wishlist'.")
    end
  elseif msg == "reset-spec" or msg == "resetspec" then
    if LootWishlist.Alerts and LootWishlist.Alerts.ResetSpecReminderDebounce then
      LootWishlist.Alerts.ResetSpecReminderDebounce()
      print("Loot Wishlist: spec reminder reset. Target a boss or re-enter to trigger again.")
    else
      print("Loot Wishlist: alerts module not ready.")
    end
  elseif msg:match("^testdrop ") then
    local arg = msg:match("^testdrop%s+(.+)$")
    if arg then
      if LootWishlist.Alerts and LootWishlist.Alerts.TestDrop then
        LootWishlist.Alerts.TestDrop(arg, false)
      end
    else
      print("/wishlist testdrop <itemID|itemLink>")
    end
  elseif msg:match("^testdropnot ") or msg:match("^testdrop%-not ") then
    local arg = msg:match("^testdrop%-?not%s+(.+)$")
    if arg then
      if LootWishlist.Alerts and LootWishlist.Alerts.TestDrop then
        LootWishlist.Alerts.TestDrop(arg, true)
      end
    else
      print("/wishlist testdrop-not <itemID|itemLink>")
    end
  elseif msg:match("^testdrop%-other ") then
    local arg = msg:match("^testdrop%-other%s+(.+)$")
    if arg then
      if LootWishlist.Alerts and LootWishlist.Alerts.TestDropOther then
        LootWishlist.Alerts.TestDropOther(arg)
      end
    else
      print("/wishlist testdrop-other <itemID|itemLink> [looterName]")
    end
  elseif msg == "raidinfo" then
    local P = "|cffC9A84CLoot Wishlist|r: "
    -- Combat status
    print(P .. "In combat: " .. tostring(InCombatLockdown()))

    -- Instance info
    local name, instType, diffID, diffName, maxPlayers, dynDiff, isDynamic, instID, instGroupSize =
      GetInstanceInfo()
    local inInstance, iType = IsInInstance()
    print(P .. "IsInInstance: " .. tostring(inInstance) .. "  type: " .. tostring(iType))
    print(P .. "Instance: " .. tostring(name) .. "  (ID " .. tostring(instID) .. ")")
    print(P .. "Difficulty: " .. tostring(diffName) .. "  (ID " .. tostring(diffID) .. ", max " .. tostring(maxPlayers) .. ")")

    if not (inInstance and (iType == "raid" or iType == "party")) then
      print(P .. "Not in a raid/dungeon instance.")
    else
      -- Current map bosses
      local mapID = C_Map and C_Map.GetBestMapForUnit and C_Map.GetBestMapForUnit("player")
      print(P .. "Player mapID: " .. tostring(mapID))

      -- Method 1: GetBossInfo (built-in boss frames, 1-indexed)
      print(P .. "--- GetBossInfo (boss frames) ---")
      local foundBossFrame = false
      for i = 1, 10 do
        local uid = "boss" .. i
        if UnitExists(uid) then
          foundBossFrame = true
          local bName = UnitName(uid)
          local bHP = UnitHealth(uid)
          local bMaxHP = UnitHealthMax(uid)
          print(P .. "  boss" .. i .. ": " .. tostring(bName)
            .. "  HP: " .. tostring(bHP) .. "/" .. tostring(bMaxHP))
        end
      end
      if not foundBossFrame then print(P .. "  (no boss frames active)") end

      -- Method 2: Saved lockout encounters (shows kill status per boss)
      print(P .. "--- Saved lockout encounters ---")
      local numSaved = GetNumSavedInstances()
      local foundLockout = false
      for i = 1, numSaved do
        local iName, iID, iReset, iDiff, iLocked, iExtended, iInstIDontUse,
              iIsRaid, iMaxPlayers, iDiffName = GetSavedInstanceInfo(i)
        if iName == name then
          foundLockout = true
          print(P .. "  Lockout: " .. tostring(iName) .. " " .. tostring(iDiffName)
            .. " (locked=" .. tostring(iLocked) .. ")")
          -- Probe how many encounters by iterating until name is nil
          local ok2, err2 = pcall(function()
            for j = 1, 20 do
              local bName, _, isKilled = GetSavedInstanceEncounterInfo(i, j)
              if not bName then break end
              local status = isKilled and "|cff69db7cKilled|r" or "|cffff6b6bAlive|r"
              print(P .. "    " .. tostring(j) .. ". " .. tostring(bName) .. ": " .. status)
            end
          end)
          if not ok2 then
            print(P .. "    GetSavedInstanceEncounterInfo error: " .. tostring(err2))
          end
        end
      end
      if not foundLockout then print(P .. "  (no matching lockout found)") end

      -- Method 3: EJ instance bosses (all bosses in the journal for this instance)
      print(P .. "--- EJ instance bosses ---")
      -- Try to find the EJ instanceID for the current raid
      local ejInstID = nil
      if EJ_GetCurrentInstance then
        ejInstID = EJ_GetCurrentInstance()
        print(P .. "  EJ_GetCurrentInstance: " .. tostring(ejInstID))
      end
      if (not ejInstID or ejInstID == 0) and EJ_GetInstanceForMap then
        ejInstID = EJ_GetInstanceForMap(mapID or 0)
        print(P .. "  EJ_GetInstanceForMap(" .. tostring(mapID) .. "): " .. tostring(ejInstID))
      end
      if ejInstID and ejInstID > 0 then
        EJ_SelectInstance(ejInstID)
        local i = 1
        while true do
          local eName, eDesc, eEncID, eRootSectionID, eLink = EJ_GetEncounterInfoByIndex(i, ejInstID)
          if not eName then break end
          print(P .. "  " .. i .. ". " .. tostring(eName) .. " (encounterID " .. tostring(eEncID) .. ")")
          i = i + 1
        end
        if i == 1 then print(P .. "  (no EJ encounters found)") end
      else
        print(P .. "  (could not determine EJ instanceID)")
      end

      -- Method 4: Encounters on current map
      print(P .. "--- Encounters on current map ---")
      if mapID and C_EncounterJournal and C_EncounterJournal.GetEncountersOnMap then
        local encounters = C_EncounterJournal.GetEncountersOnMap(mapID)
        if encounters and #encounters > 0 then
          for _, enc in ipairs(encounters) do
            local ejName = EJ_GetEncounterInfo and EJ_GetEncounterInfo(enc.encounterID) or "?"
            print(P .. "  " .. tostring(enc.encounterID) .. " - " .. tostring(ejName))
          end
        else
          print(P .. "  (none on this map)")
        end
      end
    end
  elseif msg == "clear" then
    if LootWishlist.ClearAllTracked then LootWishlist.ClearAllTracked() end
    print("Loot Wishlist: cleared all tracked items")
  else
    print("/wishlist commands: show | hide | settings | remove <ID> | list | clear | debug | reset-spec | raidinfo")
  end
end
