---@diagnostic disable: undefined-global
-- luacheck: globals CreateFrame UIParent DEFAULT_CHAT_FRAME SlashCmdList InCombatLockdown C_LFGList GameTooltip GetTime C_Spell C_SpellBook LibStub Settings C_Timer C_AddOns SecondsToTime ChallengesFrame TELEPORT_TO_DUNGEON READY SPELL_FAILED_NOT_KNOWN

local ADDON_NAME = ...

-- Cache frequently used Lua functions for performance
local pairs, ipairs, type, tostring, tonumber = pairs, ipairs, type, tostring, tonumber
local floor, format = math.floor, string.format

-- Dev guard: stop if not in WoW client.
if not CreateFrame or not UIParent then
  if print then print("[TPB] WoW API not available; skipping addon init.") end
  return
end

-- Defaults
local DEFAULTS = {
  pos = { point = "CENTER", x = 0, y = 250 },
  size = { w = 50, h = 50 },
  locked = true,
  fontKey = nil, -- LibSharedMedia font key; nil means addon default
  fontSize = 25,
  onlyWhenReady = false, -- When true, only show button when teleport cooldown is ready
}

TPB_DB = TPB_DB or {}

local function mergeDefaults(db, defaults)
  db = db or {}
  for k, v in pairs(defaults) do
    if type(v) == "table" then
      if type(db[k]) ~= "table" then db[k] = {} end
      mergeDefaults(db[k], v)
    elseif db[k] == nil then
      db[k] = v
    end
  end
  return db
end

-- Midnight Season 1 Dungeons
-- Icon is fetched dynamically via C_Spell.GetSpellTexture(spellID)
local DUNGEONS = {
  { name = "Windrunner Spire",        spell = 1254400, triggers = { "windrunner", "spire" } },
  { name = "Skyreach",                spell = 1254557, triggers = { "skyreach" } },
  { name = "Seat of the Triumvirate", spell = 1254551, triggers = { "seat", "triumvirate" } },
  { name = "Pit of Saron",            spell = 1254555, triggers = { "pit of saron", "saron", "pit" } },
  { name = "Nexus-Point Xenas",       spell = 1254563, triggers = { "nexus", "xenas" } },
  { name = "Maisara Caverns",         spell = 1254559, triggers = { "maisara", "caverns" } },
  { name = "Magisters' Terrace",      spell = 1254572, triggers = { "magister", "terrace" } },
  { name = "Al'gethar Academy",       spell = 393273,  triggers = { "algethar", "academy" } },
}

-- Activity ID to Dungeon Index Map
local ACTIVITY_TO_DUNGEON = {
  -- [ID] = Index in DUNGEONS table
  [1542] = 1, -- Windrunner Spire
  [182]  = 2, -- Skyreach
  [486]  = 3, -- Seat of the Triumvirate
  [1770] = 4, -- Pit of Saron
  [1768] = 5, -- Nexus-Point Xenas
  [1764] = 6, -- Maisara Caverns
  [1760] = 7, -- Magisters' Terrace
  [1160] = 8, -- Al'gethar Academy
}

-- MapChallengeMode ID to Spell ID for M+ UI overlay buttons
local MAP_ID_TO_SPELL = {
  -- Midnight Season 1
  [558] = 1254572, -- Magisters' Terrace
  [560] = 1254559, -- Maisara Caverns
  [559] = 1254563, -- Nexus-Point Xenas
  [557] = 1254400, -- Windrunner Spire
  [402] = 393273,  -- Al'gethar Academy
  [556] = 1254555, -- Pit of Saron
  [239] = 1254551, -- Seat of the Triumvirate
  [161] = 1254557, -- Skyreach
}

local function Print(msg)
  if DEFAULT_CHAT_FRAME then
    DEFAULT_CHAT_FRAME:AddMessage("|cff9be28f[TPB]|r " .. tostring(msg))
  else
    print("[TPB] " .. tostring(msg))
  end
end

-- Spell name helper
local function getSpellName(spellID)
  if not spellID then return nil end
  return C_Spell.GetSpellName(spellID)
end

-- Spell knowledge helper (12.0.5: uses C_SpellBook.IsSpellKnownOrInSpellBook)
local function isTeleportKnown(spellID)
  if not spellID then return false end
  if C_SpellBook and C_SpellBook.IsSpellKnownOrInSpellBook then
    return C_SpellBook.IsSpellKnownOrInSpellBook(spellID)
  end
  return false
end

-- Check if the teleport spell is off cooldown (i.e. ready to cast)
local function isTeleportReady(spellID)
  if not spellID then return false end
  if not isTeleportKnown(spellID) then return false end
  local ready = true
  pcall(function()
    local info = C_Spell.GetSpellCooldown(spellID)
    if info and info.startTime and info.duration and info.duration > 1.5 then
      local remaining = info.startTime + info.duration - GetTime()
      if remaining > 0 then
        ready = false
      end
    end
  end)
  return ready
end

local function norm(s)
  -- WoW may pass secret/tainted strings from protected LFG APIs that cannot be
  -- indexed or manipulated in any way. pcall lets us fail gracefully.
  local ok, result = pcall(function()
    s = (s or ""):lower()
    s = s:gsub("[^%a%d%'%s]", " ")
    s = s:gsub("%s+", " ")
    return s
  end)
  return ok and result or ""
end

local function matchDungeonFromListingName(name)
  local ln = norm(name)
  for _, d in ipairs(DUNGEONS) do
    for _, trig in ipairs(d.triggers) do
      if ln:find(trig, 1, true) then
        return d
      end
    end
  end
  return nil
end

local function matchDungeonFromActivity(activityID, fallbackName)
  -- Wrap in pcall: LFG API values can be tainted/secret during raids
  local ok, result = pcall(function()
    if activityID then
      -- Direct static mapping gets priority for exactness
      if ACTIVITY_TO_DUNGEON[activityID] then
        return DUNGEONS[ACTIVITY_TO_DUNGEON[activityID]]
      end
      local full
      if C_LFGList and C_LFGList.GetActivityFullName then
        full = C_LFGList.GetActivityFullName(activityID)
      end
      if not full then
        local a = C_LFGList.GetActivityInfoTable and C_LFGList.GetActivityInfoTable(activityID)
        if a then full = a.fullName or a.shortName end
      end
      if full and full ~= "" then
        local d = matchDungeonFromListingName(full)
        if d then return d end
      end
    end
    return matchDungeonFromListingName(fallbackName or "")
  end)
  return ok and result or nil
end

local function matchDungeonFromActivityIDs(activityIDs)
  -- Wrap in pcall: LFG API values can be tainted/secret during raids
  local ok, result = pcall(function()
    if type(activityIDs) == "table" then
      for _, actId in pairs(activityIDs) do
        if ACTIVITY_TO_DUNGEON[actId] then
          return DUNGEONS[ACTIVITY_TO_DUNGEON[actId]]
        end
        local fullName = nil
        if C_LFGList and C_LFGList.GetActivityFullName then
          fullName = C_LFGList.GetActivityFullName(actId)
        end
        if not fullName or fullName == "" then
          local a = C_LFGList.GetActivityInfoTable and C_LFGList.GetActivityInfoTable(actId)
          if a then fullName = a.fullName or a.shortName end
        end
        if type(fullName) == "string" and fullName:find("(Mythic Keystone)", 1, true) then
          local d = matchDungeonFromListingName(fullName)
          if d then return d end
        end
      end
    end
    return nil
  end)
  return ok and result or nil
end

-- Secure Button
---@diagnostic disable-next-line: redundant-parameter
local btn = CreateFrame("Button", "TPB_Button", UIParent, "SecureActionButtonTemplate")
btn:SetFrameStrata("MEDIUM")
btn:Hide()
btn:SetMovable(true)
btn:EnableMouse(true)
btn:RegisterForDrag("LeftButton")
btn:SetClampedToScreen(true)
-- Only allow secure action on left click; still listen to right-click for our script
btn:RegisterForClicks("LeftButtonDown", "LeftButtonUp")

local icon = btn:CreateTexture(nil, "ARTWORK")
icon:SetAllPoints(btn)
icon:Hide() -- use button normal texture like portal-buttons

-- Cooldown
---@diagnostic disable-next-line: redundant-parameter
local cd = CreateFrame("Cooldown", nil, btn, "CooldownFrameTemplate")
cd:SetAllPoints(btn)
-- Keep default swipe texture; some client builds error on nil swipes
cd:EnableMouse(false)
if cd.SetDrawEdge then cd:SetDrawEdge(false) end

-- Highlights
-- Only show strong highlight while pressing; use weaker border glow on hover
btn:SetHighlightTexture("")
btn:SetNormalTexture("")
btn:SetPushedTexture("Interface\\Buttons\\CheckButtonHilight")
do
  local pt = btn:GetPushedTexture()
  if pt and pt.SetBlendMode then pt:SetBlendMode("ADD") end
end

-- Text Overlay
local FONT_PATH = "Interface\\AddOns\\LFGTeleportButtonMidnight\\BebasNeue-Regular.ttf"
local overlayFont = CreateFont("TPB_OverlayFont")
do
  local ok = overlayFont:SetFont(FONT_PATH, 25, "OUTLINE")
  if not ok then
    local default, size, flags = GameFontNormal:GetFont()
    overlayFont:SetFont(default or "Fonts\\FRIZQT__.TTF", 25, "OUTLINE")
  end
end
local overlayText = btn:CreateFontString(nil, "OVERLAY")
overlayText:SetFontObject(overlayFont)
overlayText:SetTextColor(1, 1, 1, 1)
overlayText:SetJustifyH("CENTER")
overlayText:SetJustifyV("BOTTOM")
-- Keep the bottom of the text 35px above the icon/button top so it never overlaps
overlayText:ClearAllPoints()
overlayText:SetPoint("BOTTOM", btn, "TOP", 0, 5)

-- Hover Glow
local hoverGlow = btn:CreateTexture(nil, "HIGHLIGHT")
hoverGlow:SetTexture("Interface\\Buttons\\CheckButtonHilight")
hoverGlow:SetAllPoints(btn)
hoverGlow:SetBlendMode("ADD")
-- Tint hover with rgba(146,171,255,255) and lower alpha while hovering
hoverGlow:SetVertexColor(146 / 255, 171 / 255, 255 / 255, 1)
hoverGlow:SetAlpha(0.6)
hoverGlow:Hide()

local currentDungeon        = nil
local currentSpellID        = nil
local currentApplicationID  = nil -- Track which application the button is showing for
local pendingApply          = false
local pendingSecureUpdate   = false
local suppressedByCombat    = false
local hiddenForCombat       = false -- Auto-hide during combat
local userDismissed         = false
local notLearnedSoundPlayed = false
local showGitGudUntil       = nil
local NOT_LEARNED_SFX       = "Interface\\AddOns\\LFGTeleportButtonMidnight\\SHaoKhanlaughing.mp3"
local currentIconID         = nil
local currentSource         = "invite" -- "invite" or "party" to control overlay text
local pendingInvites        = {} -- Store multiple pending invites: [searchResultID] = dungeon

-- Returns the appropriate overlay prefix based on how the button was triggered
local function getOverlayPrefix()
  if currentSource == "party" then
    return "Your party has joined to :\n|cff07f8ab"
  end
  return "You have been invited to :\n|cff07f8ab"
end

-- Unknown teleport suffix helper
local function getUnknownSuffix(known)
  if known then return "" end
  local now = GetTime and GetTime() or 0
  if showGitGudUntil and now < showGitGudUntil then
    return "\n|cffff0000 Git Gud|r"
  end
  return "\n|cffff0000 No Teleport Yet|r"
end

-- LSM Support
local lsm = LibStub and LibStub("LibSharedMedia-3.0", true) or nil

local function applyFontFromDB()
  local size = (TPB_DB and TPB_DB.fontSize) or DEFAULTS.fontSize
  local path = FONT_PATH
  if lsm and TPB_DB and TPB_DB.fontKey then
    local fetched = lsm:Fetch("font", TPB_DB.fontKey, true)
    if type(fetched) == "string" and fetched ~= "" then
      path = fetched
    end
  end
  local ok = overlayFont:SetFont(path, size, "OUTLINE")
  if not ok then
    local default = select(1, GameFontNormal:GetFont())
    overlayFont:SetFont(default or "Fonts\\FRIZQT__.TTF", size, "OUTLINE")
  end
  if overlayText and overlayText.SetFont then
    local applied = overlayText:SetFont(path, size, "OUTLINE")
    if not applied then
      local default = select(1, GameFontNormal:GetFont())
      overlayText:SetFont(default or "Fonts\\FRIZQT__.TTF", size, "OUTLINE")
    end
  end
end

-- Forward declaration so UI can call it before it's defined
local applyPositionAndSizeFromDB

-- Options Panel
local optionsPanel
local standaloneOptionsFrame
local standaloneLSMCallbacksHooked = false

local function EnsureStandaloneOptions()
  if standaloneOptionsFrame then
    standaloneOptionsFrame:Show()
    return
  end

  ---@diagnostic disable-next-line: redundant-parameter
  local frame = CreateFrame("Frame", nil, UIParent)
  frame:SetSize(450, 420)
  frame:SetPoint("CENTER")
  frame:SetMovable(true)
  frame:EnableMouse(true)
  frame:RegisterForDrag("LeftButton")
  frame:SetScript("OnDragStart", function(self) self:StartMoving() end)
  frame:SetScript("OnDragStop", function(self) self:StopMovingOrSizing() end)
  frame:SetClampedToScreen(true)
  frame:SetAlpha(1)
  frame:SetFrameStrata("DIALOG")

  -- Subtle background without border
  local bg = frame:CreateTexture(nil, "BACKGROUND")
  bg:SetAllPoints(true)
  bg:SetColorTexture(0, 0, 0, 0.70)

  -- Title
  local title = frame:CreateFontString(nil, "ARTWORK", "GameFontNormalLarge")
  title:ClearAllPoints()
  title:SetPoint("TOP", frame, "TOP", 0, -48)
  title:SetText("|TInterface\\AddOns\\LFGTeleportButtonMidnight\\Media\\portal.blp:20|t LFG Teleport Button - Options")

  -- Close button
  ---@diagnostic disable-next-line: redundant-parameter
  local closeBtn = CreateFrame("Button", nil, frame, "UIPanelButtonTemplate")
  closeBtn:SetSize(22, 22)
  closeBtn:ClearAllPoints()
  closeBtn:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -10, -10)
  closeBtn:SetText("X")
  closeBtn:SetScript("OnClick", function() frame:Hide() end)

  -- Preview
  local preview = frame:CreateFontString(nil, "ARTWORK", "GameFontHighlight")
  if preview and preview.SetFontObject and GameFontHighlight then
    preview:SetFontObject(GameFontHighlight)
  end
  preview:ClearAllPoints()
  preview:SetPoint("TOP", title, "BOTTOM", 0, -16)
  preview:SetTextColor(1, 1, 1)
  preview:SetText("You have been invited to :\nExample Dungeon\n|cffff0000 No Teleport Yet|r")

  local function refreshPreview()
    local size = 20
    local path = FONT_PATH
    if lsm and TPB_DB and TPB_DB.fontKey then
      local fetched = lsm:Fetch("font", TPB_DB.fontKey, true)
      if type(fetched) == "string" and fetched ~= "" then
        path = fetched
      end
    end
    local ok = preview:SetFont(path, size, "OUTLINE")
    if not ok then
      local default = select(1, GameFontNormal:GetFont())
      preview:SetFont(default or "Fonts\\FRIZQT__.TTF", size, "OUTLINE")
    end
  end

  -- Font selector
  local label = frame:CreateFontString(nil, "ARTWORK", "GameFontNormal")
  label:ClearAllPoints()
  label:SetPoint("TOP", preview, "BOTTOM", 0, -16)
  label:SetText("")

  local AceGUI_local = LibStub and LibStub("AceGUI-3.0", true)
  local fontWidgetRefStandalone = nil
  local fontAnchor = label
  local fontWidget = AceGUI_local and AceGUI_local.Create and AceGUI_local:Create("LSM30_Font")
  if fontWidget then
    fontWidgetRefStandalone = fontWidget
    fontWidget:SetLabel("Font")
    if lsm and lsm.HashTable then
      fontWidget:SetList(lsm:HashTable("font"))
    end
    fontWidget:SetValue(TPB_DB.fontKey or "")
    fontWidget:SetCallback("OnValueChanged", function(widget, event, key)
      TPB_DB.fontKey = key ~= "" and key or nil
      if fontWidgetRefStandalone and fontWidgetRefStandalone.SetValue then
        fontWidgetRefStandalone:SetValue(key or "")
      end
      applyFontFromDB()
      refreshPreview()
    end)
    local fw = fontWidget.frame
    fw:SetParent(frame)
    fw:ClearAllPoints()
    fw:SetPoint("TOP", label, "BOTTOM", 0, -8)
    fw:SetWidth(300)
    fw:Show()
    fontAnchor = fw
  else
    ---@diagnostic disable-next-line: redundant-parameter
    local fontSelectBtn = CreateFrame("Button", nil, frame, "UIPanelButtonTemplate")
    fontSelectBtn:SetPoint("TOP", label, "BOTTOM", 0, -8)
    fontSelectBtn:SetSize(240, 24)
    fontSelectBtn:SetText("Font (LSM required)")
    fontSelectBtn:SetScript("OnClick", function()
      print("[TPB] AceGUI-3.0-SharedMediaWidgets not loaded; cannot show font menu.")
    end)
    fontAnchor = fontSelectBtn
  end

  -- Font size
  local sizeWidgetStandalone, sizeValueLabelStandalone, sizeSliderStandalone
  sizeWidgetStandalone = AceGUI_local and AceGUI_local.Create and AceGUI_local:Create("Slider")
  if sizeWidgetStandalone then
    sizeWidgetStandalone:SetLabel("Font Size")
    sizeWidgetStandalone:SetSliderValues(8, 72, 1)
    sizeWidgetStandalone:SetValue((TPB_DB and TPB_DB.fontSize) or DEFAULTS.fontSize)
    local swf = sizeWidgetStandalone.frame
    swf:SetParent(frame)
    swf:ClearAllPoints()
    swf:SetPoint("TOP", fontAnchor, "BOTTOM", 0, -24)
    swf:SetWidth(300)
    swf:Show()
    sizeValueLabelStandalone = frame:CreateFontString(nil, "ARTWORK", "GameFontNormalSmall")
    sizeValueLabelStandalone:SetPoint("TOP", swf, "BOTTOM", 0, -4)
    sizeValueLabelStandalone:SetText(tostring((TPB_DB and TPB_DB.fontSize) or DEFAULTS.fontSize))
    sizeWidgetStandalone:SetCallback("OnValueChanged", function(widget, event, value)
      value = tonumber(value) or ((TPB_DB and TPB_DB.fontSize) or DEFAULTS.fontSize)
      value = floor(value + 0.5)
      TPB_DB.fontSize = value
      if sizeValueLabelStandalone then sizeValueLabelStandalone:SetText(tostring(value)) end
      applyFontFromDB()
    end)
  else
    ---@diagnostic disable-next-line: redundant-parameter
    sizeSliderStandalone = CreateFrame("Slider", nil, frame, "OptionsSliderTemplate")
    sizeSliderStandalone:SetPoint("TOP", fontAnchor, "BOTTOM", 0, -24)
    sizeSliderStandalone:SetMinMaxValues(8, 72)
    sizeSliderStandalone:SetObeyStepOnDrag(true)
    sizeSliderStandalone:SetValueStep(1)
    sizeSliderStandalone:SetWidth(240)
    _G[sizeSliderStandalone:GetName() .. 'Low']:SetText('8')
    _G[sizeSliderStandalone:GetName() .. 'High']:SetText('72')
    _G[sizeSliderStandalone:GetName() .. 'Text']:SetText('Font Size')
    sizeSliderStandalone:SetScript("OnValueChanged", function(self, value)
      value = floor(value + 0.5)
      TPB_DB.fontSize = value
      applyFontFromDB()
    end)
    sizeValueLabelStandalone = frame:CreateFontString(nil, "ARTWORK", "GameFontNormalSmall")
    sizeValueLabelStandalone:SetPoint("TOP", sizeSliderStandalone, "BOTTOM", 0, -4)
    sizeValueLabelStandalone:SetText(tostring((TPB_DB and TPB_DB.fontSize) or DEFAULTS.fontSize))
  end

  -- "Show only when ready" checkbox
  ---@diagnostic disable-next-line: redundant-parameter
  local readyOnlyCheck = CreateFrame("CheckButton", nil, frame, "UICheckButtonTemplate")
  readyOnlyCheck:SetPoint("TOP", (sizeValueLabelStandalone or fontAnchor), "BOTTOM", -100, -16)
  readyOnlyCheck:SetSize(26, 26)
  local readyOnlyLabel = frame:CreateFontString(nil, "ARTWORK", "GameFontNormal")
  readyOnlyLabel:SetPoint("LEFT", readyOnlyCheck, "RIGHT", 4, 0)
  readyOnlyLabel:SetText("Show only when cooldown is ready")
  readyOnlyCheck:SetChecked(TPB_DB.onlyWhenReady or false)
  readyOnlyCheck:SetScript("OnClick", function(self)
    TPB_DB.onlyWhenReady = self:GetChecked()
    if TPB_DB.onlyWhenReady then
      Print("Will only show button when teleport cooldown is ready.")
      -- If button is currently shown and spell is not ready, hide it
      if currentSpellID and btn:IsShown() and not isTeleportReady(currentSpellID) then
        if not InCombatLockdown() then
          btn:Hide()
        end
      end
    else
      Print("Will show button regardless of cooldown state.")
      -- If we have a dungeon tracked but button is hidden, show it
      if currentDungeon and not btn:IsShown() and not userDismissed and not InCombatLockdown() then
        applyDungeon(currentDungeon)
      end
    end
  end)

  -- Drag hint
  local dragHint = frame:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall")
  dragHint:ClearAllPoints()
  dragHint:SetPoint("BOTTOM", frame, "BOTTOM", 0, 30)
  dragHint:SetText("Hold CTRL and Left-Drag the button to move it")

  -- Reset position
  ---@diagnostic disable-next-line: redundant-parameter
  local resetBtn = CreateFrame("Button", nil, frame, "UIPanelButtonTemplate")
  resetBtn:ClearAllPoints()
  resetBtn:SetPoint("BOTTOM", frame, "BOTTOM", 0, 5)
  resetBtn:SetSize(140, 24)
  resetBtn:SetText("Reset Position")
  resetBtn:SetScript("OnClick", function()
    TPB_DB.pos.point = DEFAULTS.pos.point
    TPB_DB.pos.x = DEFAULTS.pos.x
    TPB_DB.pos.y = DEFAULTS.pos.y
    if type(applyPositionAndSizeFromDB) == "function" then
      applyPositionAndSizeFromDB()
    else
      btn:SetSize(TPB_DB.size.w, TPB_DB.size.h)
      btn:ClearAllPoints()
      btn:SetPoint(TPB_DB.pos.point, UIParent, TPB_DB.pos.point, TPB_DB.pos.x, TPB_DB.pos.y)
    end
    if not btn:IsShown() then btn:Show() end
    if not currentIconID then
      currentIconID = 135751
      if overlayText and overlayText.SetText then
        local title = "Test Dungeon"
        local suffix = getUnknownSuffix(false)
        overlayText:SetText(getOverlayPrefix() .. title .. "|r" .. suffix)
      end
    end
    if icon and icon.SetTexture then icon:SetTexture(currentIconID) end
    if icon and icon.Show then icon:Show() end
    if icon and icon.SetDesaturated then icon:SetDesaturated(false) end
    Print("Position reset to default.")
  end)

  frame:SetScript("OnShow", function()
    -- Sync size slider/label
    if sizeWidgetStandalone and sizeWidgetStandalone.SetValue then
      sizeWidgetStandalone:SetValue((TPB_DB and TPB_DB.fontSize) or DEFAULTS.fontSize)
      if sizeValueLabelStandalone then
        sizeValueLabelStandalone:SetText(tostring((TPB_DB and TPB_DB.fontSize) or
          DEFAULTS.fontSize))
      end
    elseif sizeSliderStandalone and sizeSliderStandalone.SetValue then
      sizeSliderStandalone:SetValue((TPB_DB and TPB_DB.fontSize) or DEFAULTS.fontSize)
      if sizeValueLabelStandalone then
        sizeValueLabelStandalone:SetText(tostring((TPB_DB and TPB_DB.fontSize) or
          DEFAULTS.fontSize))
      end
    end
    if fontWidgetRefStandalone and fontWidgetRefStandalone.SetValue then
      fontWidgetRefStandalone:SetValue(TPB_DB.fontKey or "")
    end
    -- Sync "only when ready" checkbox
    if readyOnlyCheck then
      readyOnlyCheck:SetChecked(TPB_DB.onlyWhenReady or false)
    end
    refreshPreview()
    applyFontFromDB()
  end)

  if lsm and lsm.RegisterCallback and not standaloneLSMCallbacksHooked then
    standaloneLSMCallbacksHooked = true
    lsm:RegisterCallback("LibSharedMedia_Registered", function(event, mediatype, key)
      if mediatype == "font" then
        refreshPreview()
      end
    end)
    lsm:RegisterCallback("LibSharedMedia_SetGlobal", function(event, mediatype)
      if mediatype == "font" then
        applyFontFromDB()
        refreshPreview()
      end
    end)
  end

  standaloneOptionsFrame = frame
  frame:Show()
end
local function InitOptionsPanel()
  if optionsPanel then return end
  optionsPanel = CreateFrame("Frame", nil)
  local displayName = "|TInterface\\AddOns\\LFGTeleportButtonMidnight\\Media\\portal.blp:16|t LFG Teleport Button"
  optionsPanel.name = displayName

  -- Open Config Button
  ---@diagnostic disable-next-line: redundant-parameter
  local openStandaloneBtn = CreateFrame("Button", nil, optionsPanel, "UIPanelButtonTemplate")
  openStandaloneBtn:SetPoint("CENTER", optionsPanel, "CENTER", 0, 0)
  openStandaloneBtn:SetSize(200, 48)
  openStandaloneBtn:SetText("Open Config")
  do
    local fs = openStandaloneBtn:GetFontString()
    if fs and fs.GetFont then
      local fontPath, fontSize, fontFlags = fs:GetFont()
      local newSize = floor((fontSize or 14) * 2)
      fs:SetFont(fontPath or "Fonts\\FRIZQT__.TTF", newSize, fontFlags)
    end
  end
  openStandaloneBtn:SetScript("OnClick", function()
    EnsureStandaloneOptions()
  end)

  if Settings and Settings.RegisterCanvasLayoutCategory then
    local category = Settings.RegisterCanvasLayoutCategory(optionsPanel, optionsPanel.name)
    Settings.RegisterAddOnCategory(category)
  else
    -- Settings API check
    -- as we are strictly targeting modern retail.
    print("[TPB] Settings API not found. Options panel not registered.")
  end
end

local function updateMovable()
  -- Drag handling
  btn:SetScript("OnDragStart", function(self)
    if IsControlKeyDown and IsControlKeyDown() and not InCombatLockdown() then
      self:StartMoving()
    end
  end)
  btn:SetScript("OnDragStop", function(self)
    self:StopMovingOrSizing()
    local p, _, _, x, y = self:GetPoint()
    TPB_DB.pos.point = p; TPB_DB.pos.x = x; TPB_DB.pos.y = y
  end)
end

-- ALT+RightClick to hide
btn:SetScript("OnMouseUp", function(self, mouseButton)
  -- Play sound on unlearned click
  if mouseButton == "LeftButton" then
    if currentSpellID and not isTeleportKnown(currentSpellID) and not notLearnedSoundPlayed then
      if PlaySoundFile then
        PlaySoundFile(NOT_LEARNED_SFX, "Master")
      end
      notLearnedSoundPlayed = true
      -- Show Git Gud for 3 seconds
      if GetTime then showGitGudUntil = GetTime() + 3 end
      -- Update overlay to reflect the new message once the sound condition is met
      local title = (currentDungeon and currentDungeon.name) or "Teleport"
      overlayText:SetText(getOverlayPrefix() .. title .. "|r" .. getUnknownSuffix(false))
      -- Schedule text refresh after Git Gud expires
      if C_Timer and C_Timer.After then
        C_Timer.After(3.1, function()
          if currentDungeon and btn:IsShown() then
            local t = currentDungeon.name or "Teleport"
            local k = currentSpellID and isTeleportKnown(currentSpellID)
            overlayText:SetText(getOverlayPrefix() .. t .. "|r" .. getUnknownSuffix(k))
          end
        end)
      end
    end
  end
  if mouseButton == "RightButton" and (IsAltKeyDown and IsAltKeyDown()) then
    if GameTooltip and GameTooltip:IsOwned(self) then GameTooltip:Hide() end
    userDismissed = true
    -- Clear all state when user manually dismisses
    currentApplicationID = nil
    currentDungeon = nil
    currentSpellID = nil
    currentIconID = nil
    pendingApply = false
    pendingSecureUpdate = false
    suppressedByCombat = false
    pendingInvites = {}
    if InCombatLockdown and InCombatLockdown() then
      -- Avoid protected Show/Hide in combat; fade out instead
      self:SetAlpha(0)
      suppressedByCombat = true
    else
      self:Hide()
    end
  end
end)

-- Tooltip
btn:SetScript("OnEnter", function(self)
  if not GameTooltip then return end
  GameTooltip:SetOwner(self, "ANCHOR_BOTTOMRIGHT")
  GameTooltip:SetText("Left Click to Teleport", 1, 1, 1)
  GameTooltip:AddLine("ALT + Right Click to Close", 1, 1, 1)
  GameTooltip:AddLine("CTRL + Left Click to Drag", 0.8, 0.8, 0.8)
  -- No chat command hints
  GameTooltip:Show()
  if hoverGlow then hoverGlow:Show() end
end)

btn:SetScript("OnLeave", function()
  if GameTooltip then GameTooltip:Hide() end
  if hoverGlow then hoverGlow:Hide() end
end)


local function updateKnownVisual()
  if not currentSpellID then return end
  -- Keep button fully opaque
  btn:SetAlpha(1)
  local known = isTeleportKnown(currentSpellID)
  if currentIconID then
    icon:SetTexture(currentIconID)
  end
  icon:Show()
  if icon and icon.SetDesaturated then icon:SetDesaturated(not known) end
end

local function updateCooldown()
  if not currentSpellID then return end

  -- "Show only when ready" mode: hide button when on cooldown, show when ready
  if TPB_DB.onlyWhenReady and currentDungeon and not userDismissed then
    local ready = isTeleportReady(currentSpellID)
    if not InCombatLockdown() then
      if ready and not btn:IsShown() then
        -- Cooldown just finished — show the button now
        btn:SetAttribute("type2", nil)
        local iconID = C_Spell.GetSpellTexture(currentSpellID) or 135744
        currentIconID = iconID
        icon:SetTexture(iconID)
        icon:Show()
        btn:SetAttribute("type", "spell")
        btn:SetAttribute("spell", currentSpellID)
        updateKnownVisual()
        btn:ClearAllPoints()
        btn:SetPoint(TPB_DB.pos.point, UIParent, TPB_DB.pos.point, TPB_DB.pos.x, TPB_DB.pos.y)
        btn:SetSize(TPB_DB.size.w, TPB_DB.size.h)
        btn:Show()
        local overlayTitle = (currentDungeon.name or "Teleport")
        overlayText:SetText(getOverlayPrefix() .. overlayTitle .. "|r")
        Print("Teleport ready: " .. (currentDungeon.name or "unknown"))
      elseif not ready and btn:IsShown() then
        -- Spell went on cooldown — hide the button
        btn:Hide()
      end
    end
    if not ready then return end -- Don't update cooldown display if not ready
  end

  -- Wrap entire cooldown logic in pcall to handle "secret values" during combat
  local success = pcall(function()
    local info = C_Spell.GetSpellCooldown(currentSpellID)
    if info and info.startTime and info.duration and info.duration > 1.5 then
      cd:SetCooldown(info.startTime, info.duration)
    elseif cd.Clear then
      cd:Clear()
    end
  end)
  if not success and cd.Clear then
    cd:Clear()
  end

  -- Update on-button text overlay
  local title = (currentDungeon and currentDungeon.name) or "Teleport"
  local known = currentSpellID and isTeleportKnown(currentSpellID)
  local suffix = getUnknownSuffix(known)
  overlayText:SetText(getOverlayPrefix() .. title .. "|r" .. suffix)
  -- Note: Combat lockdown is now handled in applyDungeon, so we don't hide here
  -- This was causing the button to disappear when SPELL_UPDATE_COOLDOWN fired during combat
  if suppressedByCombat and not InCombatLockdown() then
    btn:SetAlpha(1)
    suppressedByCombat = false
  end
end

local function applyDungeon(d)
  currentDungeon = d
  currentSpellID = d.spell

  if InCombatLockdown() then
    pendingApply = true
    return
  end

  -- Don't show if user dismissed the button
  if userDismissed then
    return
  end

  -- "Show only when ready" check: suppress if spell is not known or on cooldown
  if TPB_DB.onlyWhenReady then
    if not isTeleportReady(currentSpellID) then
      -- Don't show button; it will appear when cooldown finishes (via SPELL_UPDATE_COOLDOWN)
      return
    end
  end

  -- Secure attributes: resolve spell name via modern API
  -- Secure attributes: use ID directly for robustness
  -- Always prevent right-click from triggering secure actions
  btn:SetAttribute("type2", nil)

  -- Fetch icon dynamically from spell API (fallback to generic portal icon)
  local iconID = C_Spell.GetSpellTexture(currentSpellID) or 135744 -- 135744 = generic portal icon
  currentIconID = iconID
  icon:SetTexture(iconID)
  icon:Show()

  local known = isTeleportKnown(currentSpellID)
  if known then
    btn:SetAttribute("type", "spell")
    btn:SetAttribute("spell", currentSpellID)
  else
    -- Disarm left click if spell not known
    btn:SetAttribute("type", nil)
    btn:SetAttribute("spell", nil)
  end

  updateKnownVisual()
  updateCooldown()
  btn:ClearAllPoints()
  btn:SetPoint(TPB_DB.pos.point, UIParent, TPB_DB.pos.point, TPB_DB.pos.x, TPB_DB.pos.y)
  btn:SetSize(TPB_DB.size.w, TPB_DB.size.h)
  btn:Show()
  local overlayTitle = (d.name or "Teleport")
  local suffix = getUnknownSuffix(known)
  overlayText:SetText(getOverlayPrefix() .. overlayTitle .. "|r" .. suffix)
  Print("Teleport ready: " .. (d.name or "unknown"))
end

-- Slash: /tpbutton lock  (toggle)
SLASH_TPBUTTON1 = "/tpbutton"
-- Forward declaration defined earlier; do not redeclare here
-- local applyPositionAndSizeFromDB

SlashCmdList.TPBUTTON = function(msg)
  msg = (msg or ""):gsub("^%s+", ""):gsub("%s+$", "")
  local lower = msg:lower()

  if lower == "lock" then
    TPB_DB.locked = not TPB_DB.locked
    updateMovable()
    Print(TPB_DB.locked and "Locked (cannot drag)" or "Unlocked (drag with left mouse)")
  elseif lower == "show" then
    if currentDungeon then
      -- Reset dismissal state for show command
      userDismissed = false
      applyDungeon(currentDungeon)
    else
      Print("No dungeon selected yet; use /tpbutton test N")
    end
  elseif lower:find("^test") then
    local idx = tonumber(msg:match("^%s*test%s+(%d+)%s*$"))
    if idx and DUNGEONS[idx] then
      -- Reset dismissal state for test commands
      userDismissed = false
      applyDungeon(DUNGEONS[idx])
    else
      Print("Usage: /tpbutton test N  (N=1-" .. #DUNGEONS .. ")")
      for i, d in ipairs(DUNGEONS) do
        local known = isTeleportKnown(d.spell)
        local spellName = getSpellName(d.spell)
        Print(i ..
          ": " ..
          d.name ..
          " (spell: " .. tostring(d.spell) .. ", known: " .. tostring(known) .. ", spellName: " ..
          (spellName or "nil") .. ")")
      end
    end
  elseif lower == "debug" then
    Print("=== Dungeon Debug Info ===")
    for i, d in ipairs(DUNGEONS) do
      local known = isTeleportKnown(d.spell)
      local spellName = getSpellName(d.spell)
      local iconID = C_Spell.GetSpellTexture(d.spell) or "N/A"
      Print(i .. ": " .. d.name)
      Print("  Spell ID: " .. tostring(d.spell))
      Print("  -> isTeleportKnown: " .. tostring(known))
      Print("  -> C_SpellBook.IsSpellKnownOrInSpellBook: " .. tostring(C_SpellBook.IsSpellKnownOrInSpellBook(d.spell)))
      Print("  -> Name: " .. (spellName or "nil"))
      Print("  -> Icon (from API): " .. tostring(iconID))
    end
    Print("=== Activity Mappings ===")
    for actID, dungeonIdx in pairs(ACTIVITY_TO_DUNGEON) do
      local d = DUNGEONS[dungeonIdx]
      Print("Activity " .. actID .. " -> " .. (d and d.name or "INVALID INDEX " .. dungeonIdx))
    end
  elseif lower == "reset" then
    TPB_DB.pos.point = DEFAULTS.pos.point
    TPB_DB.pos.x = DEFAULTS.pos.x
    TPB_DB.pos.y = DEFAULTS.pos.y
    applyPositionAndSizeFromDB()
    Print("Position reset to default.")
  elseif lower == "state" or lower == "status" then
    Print("=== Button State ===")
    Print("IsShown: " .. tostring(btn:IsShown()))
    Print("Alpha: " .. tostring(btn:GetAlpha()))
    Print("userDismissed: " .. tostring(userDismissed))
    Print("pendingApply: " .. tostring(pendingApply))
    Print("pendingSecureUpdate: " .. tostring(pendingSecureUpdate))
    Print("suppressedByCombat: " .. tostring(suppressedByCombat))
    Print("InCombat: " .. tostring(InCombatLockdown and InCombatLockdown() or false))
    Print("currentDungeon: " .. tostring(currentDungeon and currentDungeon.name or "nil"))
    Print("currentSpellID: " .. tostring(currentSpellID))
    Print("currentIconID: " .. tostring(currentIconID))
    Print("currentApplicationID: " .. tostring(currentApplicationID))
    Print("onlyWhenReady: " .. tostring(TPB_DB.onlyWhenReady))
    if currentSpellID then
      Print("Spell Known: " .. tostring(isTeleportKnown(currentSpellID)))
      Print("Spell Name: " .. tostring(getSpellName(currentSpellID)))
    end
  else
    Print(
      "Commands: /tpbutton lock | /tpbutton show | /tpbutton test N | /tpbutton debug | /tpbutton reset | /tpbutton state")
  end
end

-- =============================================
-- M+ UI Overlay Buttons (like DungeonTeleportButtons)
-- =============================================
local overlayButtons = {}

local function UpdateOverlayTooltip(parent, spellID, initialize)
  if not initialize and not GameTooltip:IsOwned(parent) then return end

  local Button_OnEnter = parent:GetScript("OnEnter")
  if not Button_OnEnter then return end

  local name = C_Spell.GetSpellName(spellID)
  Button_OnEnter(parent)

  if C_SpellBook and C_SpellBook.IsSpellKnownOrInSpellBook and C_SpellBook.IsSpellKnownOrInSpellBook(spellID) then
    GameTooltip:AddLine(" ")
    GameTooltip:AddLine(name or TELEPORT_TO_DUNGEON)
    -- Wrap cooldown check in pcall to handle "secret values" during combat
    local showReady = true
    pcall(function()
      local cooldownInfo = C_Spell.GetSpellCooldown(spellID)
      if cooldownInfo and cooldownInfo.duration and cooldownInfo.duration > 0 then
        local remaining = cooldownInfo.startTime + cooldownInfo.duration - GetTime()
        if remaining > 0 then
          GameTooltip:AddLine(SecondsToTime(math.ceil(remaining)), 1, 0, 0)
          showReady = false
        end
      end
    end)
    if showReady then
      GameTooltip:AddLine(READY, 0, 1, 0)
    end
  else
    GameTooltip:AddLine(" ")
    GameTooltip:AddLine(name or TELEPORT_TO_DUNGEON)
    GameTooltip:AddLine(SPELL_FAILED_NOT_KNOWN, 1, 0, 0)
  end

  GameTooltip:Show()
  C_Timer.After(1, function()
    if parent and parent:IsShown() and parent:IsMouseOver() then
      UpdateOverlayTooltip(parent, spellID)
    end
  end)
end

local function CreateOverlayButton(parent, spellID)
  if not spellID then return end

  local button = overlayButtons[parent] or CreateFrame("Button", nil, parent, "InsecureActionButtonTemplate")
  button:SetAllPoints(parent)
  button:RegisterForClicks("AnyDown", "AnyUp")
  button:SetAttribute("type", "spell")
  button:SetAttribute("spell", spellID)
  button:SetScript("OnEnter", function() UpdateOverlayTooltip(parent, spellID, true) end)
  button:SetScript("OnLeave", function() if GameTooltip:IsOwned(parent) then GameTooltip:Hide() end end)

  overlayButtons[parent] = button
end

local function CreateAllOverlayButtons()
  if InCombatLockdown() then return end
  if not ChallengesFrame then return end
  if not ChallengesFrame.DungeonIcons then return end

  for _, dungeonIcon in next, ChallengesFrame.DungeonIcons do
    CreateOverlayButton(dungeonIcon, MAP_ID_TO_SPELL[dungeonIcon.mapID])
  end
end

local function InitializeChallengesUI()
  if not C_AddOns.IsAddOnLoaded("Blizzard_ChallengesUI") then return false end

  if ChallengesFrame and type(ChallengesFrame.Update) == "function" then
    hooksecurefunc(ChallengesFrame, "Update", CreateAllOverlayButtons)
  end
  CreateAllOverlayButtons()
  return true
end

-- Event handler
local f = CreateFrame("Frame")
f:RegisterEvent("ADDON_LOADED")
f:RegisterEvent("PLAYER_LOGIN")
f:RegisterEvent("LFG_LIST_APPLICATION_STATUS_UPDATED")
f:RegisterEvent("UNIT_SPELLCAST_SUCCEEDED")
f:RegisterEvent("SPELL_UPDATE_COOLDOWN")
f:RegisterEvent("SPELLS_CHANGED")
f:RegisterEvent("PLAYER_REGEN_ENABLED")
f:RegisterEvent("PLAYER_REGEN_DISABLED")
f:RegisterEvent("GROUP_ROSTER_UPDATE")
f:RegisterEvent("CHALLENGE_MODE_START")
f:RegisterEvent("CHALLENGE_MODE_COMPLETED")
f:RegisterEvent("LFG_LIST_ACTIVE_ENTRY_UPDATE")

applyPositionAndSizeFromDB = function()
  btn:SetSize(TPB_DB.size.w, TPB_DB.size.h)
  btn:ClearAllPoints()
  btn:SetPoint(TPB_DB.pos.point, UIParent, TPB_DB.pos.point, TPB_DB.pos.x, TPB_DB.pos.y)
end

local nonMatchPrinted = {}


local function CheckActiveGroup()
  if InCombatLockdown() then return end
  if userDismissed then return end

  -- Wrap in pcall: C_LFGList returns tainted values during raids
  pcall(function()
    local info = C_LFGList.GetActiveEntryInfo()
    if info then
      -- Try activityIDs first (array of all activities), then single activityID
      local d = nil
      if info.activityIDs then
        d = matchDungeonFromActivityIDs(info.activityIDs)
      end
      if not d then
        d = matchDungeonFromActivity(info.activityID, info.name)
      end
      if d and currentDungeon ~= d then
        currentSource = "party"
        applyDungeon(d)
      end
    end
  end)
end

local events = {}

function events:ADDON_LOADED(name)
  if name == ADDON_NAME then
    TPB_DB = mergeDefaults(TPB_DB, DEFAULTS)
    -- Always start locked on load
    TPB_DB.locked = true
    updateMovable()
    -- Try to initialize M+ overlay if ChallengesUI is already loaded
    InitializeChallengesUI()
  elseif name == "Blizzard_ChallengesUI" then
    -- ChallengesUI just loaded, initialize our overlay buttons
    InitializeChallengesUI()
  end
end

function events:PLAYER_LOGIN()
  applyPositionAndSizeFromDB()
  applyFontFromDB()
  InitOptionsPanel()
  -- Reset per-login sound flag
  notLearnedSoundPlayed = false
  -- Try to initialize M+ overlay (in case it loaded before us)
  InitializeChallengesUI()
  -- Start 5s ticker for host/active group check
  C_Timer.NewTicker(5, CheckActiveGroup)
end

function events:LFG_LIST_APPLICATION_STATUS_UPDATED(searchResultID, newStatus, oldStatus, groupName)
  if newStatus == "invited" then
    -- Store dungeon info when invited, but don't show button yet
    -- Reset user dismissal state for new invitations
    userDismissed = false
    local info
    -- Resolve directly via searchResultID (first event parameter)
    if searchResultID and type(searchResultID) == "number" and searchResultID > 0 then
      pcall(function()
        info = C_LFGList.GetSearchResultInfo(searchResultID)
      end)
    end

    local listName = info and info.name or ""
    if info and (not listName or listName == "") then
      listName = (info.comment or "") .. " " .. (info.voiceChat or "")
    end
    local d = nil
    if info and info.activityIDs then
      d = matchDungeonFromActivityIDs(info.activityIDs)
    end
    if not d then
      d = matchDungeonFromActivity(info and info.activityID, listName)
    end
    if d then
      -- Store dungeon info keyed by searchResultID - supports multiple pending invites
      pendingInvites[searchResultID] = d
    else
      -- Rate-limit non-match prints
      local key = searchResultID or groupName or "unknown"
      if not nonMatchPrinted[key] then
        nonMatchPrinted[key] = true
        Print("Listing did not match a Midnight S1 dungeon; button not shown.")
      end
    end
  elseif newStatus == "inviteaccepted" or (oldStatus == "invited" and newStatus ~= "invited" and newStatus ~= "declined" and newStatus ~= "declined_full" and newStatus ~= "declined_delisted" and newStatus ~= "cancelled" and newStatus ~= "failed" and newStatus ~= "timedout" and newStatus ~= "invitedeclined") then
    -- Show button when invite is accepted
    -- Look up the correct dungeon for THIS specific searchResultID
    local d = pendingInvites[searchResultID]
    if d then
      currentSource = "invite"
      currentApplicationID = searchResultID
      applyDungeon(d)
      -- Clear all pending invites since we joined a group
      pendingInvites = {}
    end
  elseif newStatus == "declined" or newStatus == "declined_full" or newStatus == "declined_delisted" or newStatus == "cancelled" or newStatus == "failed" or newStatus == "timedout" or newStatus == "invitedeclined" then
    -- Clear rate-limit entry
    nonMatchPrinted[searchResultID or groupName] = nil
    -- Clear this specific pending invite
    pendingInvites[searchResultID] = nil
    -- Only hide the button if this status change is for the CURRENT invitation
    -- When you apply to multiple dungeons and get invited to one, WoW cancels all other applications
    -- We don't want to hide the button when those OTHER applications get cancelled
    if searchResultID == currentApplicationID then
      -- Clear all state since this invitation is done
      currentApplicationID = nil
      currentDungeon = nil
      currentSpellID = nil
      currentIconID = nil
      pendingApply = false
      pendingSecureUpdate = false
      suppressedByCombat = false
      btn:Hide()
    end
  end
end

function events:UNIT_SPELLCAST_SUCCEEDED(unit, _, spellID)
  if unit == "player" and currentSpellID and spellID == currentSpellID then
    -- Clear all state since we're done with this teleport
    currentApplicationID = nil
    currentDungeon = nil
    currentSpellID = nil
    currentIconID = nil
    pendingApply = false
    pendingSecureUpdate = false
    suppressedByCombat = false
    pendingInvites = {}
    btn:Hide()
  end
end

function events:SPELL_UPDATE_COOLDOWN()
  -- Only process if button is shown and we have a current spell
  if currentSpellID and btn:IsShown() then
    updateCooldown()
    if showGitGudUntil then
      local now = GetTime and GetTime() or 0
      if now < showGitGudUntil then
        local title = (currentDungeon and currentDungeon.name) or "Teleport"
        local known = isTeleportKnown(currentSpellID)
        overlayText:SetText(getOverlayPrefix() .. title .. "|r" .. getUnknownSuffix(known))
      end
    end
  end
end

function events:SPELLS_CHANGED()
  -- Only process if we have a current spell being tracked
  if not currentSpellID then return end

  if InCombatLockdown and InCombatLockdown() then
    pendingSecureUpdate = true
    -- Hide visuals to avoid interaction in combat
    btn:SetAlpha(0)
    suppressedByCombat = true
  else
    local known = isTeleportKnown(currentSpellID)
    if known then
      btn:SetAttribute("type", "spell")
      btn:SetAttribute("spell", currentSpellID)
    else
      btn:SetAttribute("type", nil)
      btn:SetAttribute("spell", nil)
    end
    updateKnownVisual()
    -- Refresh overlay text with red warning if needed
    local title = (currentDungeon and currentDungeon.name) or "Teleport"
    local suffix = getUnknownSuffix(known)
    overlayText:SetText(getOverlayPrefix() .. title .. "|r" .. suffix)
    -- Ensure any font changes apply to the live overlay
    applyFontFromDB()
  end
end

function events:PLAYER_REGEN_DISABLED()
  -- Combat started - auto-hide button if shown
  if btn:IsShown() and btn:GetAlpha() > 0 and currentDungeon then
    btn:SetAlpha(0)
    hiddenForCombat = true
  end
end

function events:PLAYER_REGEN_ENABLED()
  -- Combat ended - restore button if it was auto-hidden
  if hiddenForCombat then
    hiddenForCombat = false
    if currentDungeon and not userDismissed then
      btn:SetAlpha(1)
    end
  end

  -- Only process if we have pending updates or suppressed state
  if not (pendingApply or pendingSecureUpdate or suppressedByCombat) then
    -- Nothing else to do
  else
    -- Safe to apply any deferred secure updates now
    if pendingApply and currentDungeon then
      pendingApply = false
      applyDungeon(currentDungeon)
    end
    if pendingSecureUpdate and currentSpellID then
      pendingSecureUpdate = false
      local known = isTeleportKnown(currentSpellID)
      if known then
        btn:SetAttribute("type", "spell")
        btn:SetAttribute("spell", currentSpellID)
      else
        btn:SetAttribute("type", nil)
        btn:SetAttribute("spell", nil)
      end
    end
    if suppressedByCombat then
      suppressedByCombat = false
      btn:SetAlpha(1)
    end
  end
end

function events:GROUP_ROSTER_UPDATE()
  local inGroup = (IsInGroup and IsInGroup()) or (IsInRaid and IsInRaid())

  -- Reset dismiss state when player leaves a group so button can appear for next party
  if not inGroup then
    if userDismissed then
      userDismissed = false
    end
    return
  end

  -- Check if player joined a group and we have pending invites
  -- This handles cases where the status change event doesn't fire properly
  if not currentDungeon then
    -- Check if we have any pending invites
    local resultID, dungeon = next(pendingInvites)
    if resultID and dungeon then
      currentSource = "invite"
      currentApplicationID = resultID
      applyDungeon(dungeon)
      pendingInvites = {}
    else
      -- No pending invites — check if the group is already listed in LFG
      -- (e.g. joining an already-listed group won't fire LFG_LIST_ACTIVE_ENTRY_UPDATE)
      CheckActiveGroup()
    end
  end
end

function events:CHALLENGE_MODE_START()
  -- Mythic+ key activated — hide the teleport button and clear all state
  if currentDungeon or btn:IsShown() then
    currentApplicationID = nil
    currentDungeon = nil
    currentSpellID = nil
    currentIconID = nil
    pendingApply = false
    pendingSecureUpdate = false
    pendingInvites = {}
    if not InCombatLockdown() then
      btn:Hide()
      suppressedByCombat = false
    else
      btn:SetAlpha(0)
      suppressedByCombat = true
    end
  end
end

function events:CHALLENGE_MODE_COMPLETED()
  -- Belt and suspenders: also clean up when run completes
  if currentDungeon or btn:IsShown() then
    currentApplicationID = nil
    currentDungeon = nil
    currentSpellID = nil
    currentIconID = nil
    pendingApply = false
    pendingSecureUpdate = false
    pendingInvites = {}
    if not InCombatLockdown() then
      btn:Hide()
      suppressedByCombat = false
    else
      btn:SetAlpha(0)
      suppressedByCombat = true
    end
  end
end

function events:LFG_LIST_ACTIVE_ENTRY_UPDATE()
  -- Fires when your group's LFG listing is created, updated, or removed
  -- This is the instant detection path (instead of waiting for the 5s poll)
  CheckActiveGroup()
end

f:SetScript("OnEvent", function(self, event, ...)
  if events[event] then
    events[event](events, ...)
  end
end)
