--[[
  SpellSmasher secure executor - (c) Tracy Howard / SpellSmasher.

  OOC: CompileFlatSteps  >  bake spelllist into SecureHandler  >  WrapScript OnClick
  Combat: set macrotext for current step, ALWAYS advance step index.

  Once mode = once per combat engagement:
  - After an Once click fires, that flat slot is marked used (secure attributes).
  - Used Once slots are skipped for the rest of the engagement.
  - Reset when target GUID changes (retarget) or on leave combat (insecure OOC clear).
]]

local NS = SpellSmasherNS
local Secure = {}
NS.Secure = Secure

local BUTTON_NAME = NS.BUTTON_NAME
local RELAY_NAME = NS.RELAY_NAME

Secure.seqButton = nil
-- Live icon: spell name/id for current NEXT step (updated after each mash)
Secure.iconState = { spell = nil, step = 1, tex = nil }

local QUESTION_ICON = 134400 -- INV_Misc_QuestionMark

local function FlatMacro(entry)
  if NS.Compile and NS.Compile.FlatStepMacrotext then
  return NS.Compile.FlatStepMacrotext(entry)
  end
  if type(entry) == "table" then
  return tostring(entry.macrotext or "")
  end
  return tostring(entry or "")
end

local function FlatIsOnce(entry)
  if NS.Compile and NS.Compile.FlatStepIsOnce then
  return NS.Compile.FlatStepIsOnce(entry)
  end
  return type(entry) == "table" and entry.once == true
end

-- Always advance after a step; Shift modes via attribute.
-- Once slots: fire once per combat, then skip until leave combat (insecure reset).
-- NOTE: Restricted secure env has NO UnitGUID / unit APIs - do not call them here.
local SECURE_ONCLICK_WRAP = [[
    local shiftmode = self:GetAttribute("shiftmode") or "ignore"
    if IsShiftKeyDown() and shiftmode == "pause" then
        self:SetAttribute("type", "macro")
        self:SetAttribute("macro", nil)
        self:SetAttribute("unit", nil)
        self:SetAttribute("macrotext", "")
        return
    end
    local list, maxn, stepAttr, firedPrefix
    if IsShiftKeyDown() and shiftmode == "aoe" then
        list = spelllist_aoe
        maxn = max_aoe
        stepAttr = "aoe_step"
        firedPrefix = "once_aoe_"
    else
        list = spelllist_st
        maxn = max_st
        stepAttr = "step"
        firedPrefix = "once_st_"
    end
    maxn = tonumber(maxn) or 0
    if not list or maxn < 1 then
        self:SetAttribute("type", "macro")
        self:SetAttribute("macro", nil)
        self:SetAttribute("unit", nil)
        self:SetAttribute("macrotext", "/startattack")
        return
    end

    local step = self:GetAttribute(stepAttr)
    step = tonumber(step)
    if not step or step < 1 then
        step = 1
    end
    if step > maxn then
        step = 1
    end

    -- Skip Once slots already fired this combat
    local tries = 0
    while tries < maxn do
        local e = list[step]
        local fired = self:GetAttribute(firedPrefix .. step)
        if e and e["once"] == "1" and fired == 1 then
            if step < maxn then
                step = step + 1
            else
                step = 1
            end
            tries = tries + 1
        else
            break
        end
    end
    -- All Once used and nothing else to cast: no-op until leave combat resets
    if tries >= maxn then
        local e0 = list[step]
        local f0 = self:GetAttribute(firedPrefix .. step)
        if e0 and e0["once"] == "1" and f0 == 1 then
            self:SetAttribute("type", "macro")
            self:SetAttribute("macro", nil)
            self:SetAttribute("unit", nil)
            self:SetAttribute("macrotext", "")
            return
        end
    end

    local entry = list[step]
    if entry then
        for k, v in pairs(entry) do
            if k ~= "once" then
                if k == "macrotext" then
                    self:SetAttribute("macro", nil)
                    self:SetAttribute("unit", nil)
                elseif k == "macro" then
                    self:SetAttribute("macrotext", nil)
                    self:SetAttribute("unit", nil)
                end
                self:SetAttribute(k, v)
            end
        end
        if entry["once"] == "1" then
            self:SetAttribute(firedPrefix .. step, 1)
        end
    else
        self:SetAttribute("type", "macro")
        self:SetAttribute("macro", nil)
        self:SetAttribute("unit", nil)
        self:SetAttribute("macrotext", "/startattack")
    end

    -- Advance; skip already-used Once for next icon
    if step < maxn then
        step = step + 1
    else
        step = 1
    end
    tries = 0
    while tries < maxn do
        local e2 = list[step]
        local f2 = self:GetAttribute(firedPrefix .. step)
        if e2 and e2["once"] == "1" and f2 == 1 then
            if step < maxn then
                step = step + 1
            else
                step = 1
            end
            tries = tries + 1
        else
            break
        end
    end
    self:SetAttribute(stepAttr, step)
]]

local function LongBracket(s)
  s = tostring(s or ""):gsub("[{}]", "")
  local n = 7
  while true do
  local close = "]" .. string.rep("=", n) .. "]"
  if not string.find(s, close, 1, true) then
  return "[" .. string.rep("=", n) .. "[" .. s .. close
  end
  n = n + 1
  if n > 40 then
  s = s:gsub("%]=+%]", "]")
  return "[=======[" .. s .. "]=======]"
  end
  end
end

local function BakeOneList(btn, listName, maxName, macros)
  macros = macros or {}
  local CHUNK = 20
  btn:Execute(listName .. " = newtable()\n" .. maxName .. " = 0\n")
  local i = 1
  local n = #macros
  while i <= n do
  local parts = {}
  local last = math.min(n, i + CHUNK - 1)
  for j = i, last do
  local mt = FlatMacro(macros[j])
  local isOnce = FlatIsOnce(macros[j])
  parts[#parts + 1] = "tinsert(" .. listName .. ", newtable())\n"
  parts[#parts + 1] = "local i = #" .. listName .. "\n"
  parts[#parts + 1] = listName .. '[i]["type"] = ' .. LongBracket("macro") .. "\n"
  parts[#parts + 1] = listName .. '[i]["macrotext"] = ' .. LongBracket(mt) .. "\n"
  if isOnce then
  parts[#parts + 1] = listName .. '[i]["once"] = "1"\n'
  end
  end
  parts[#parts + 1] = maxName .. " = #" .. listName .. "\n"
  btn:Execute(table.concat(parts))
  i = last + 1
  end
end

--- Clear Once engagement flags (must be OOC for full clear, or call from secure).
function Secure.ResetOnceEngagement(reason)
  local btn = Secure.seqButton
  if not btn then
  return
  end
  if InCombatLockdown() then
  NS.pendingOnceReset = true
  return
  end
  NS.pendingOnceReset = false
  local maxn = 0
  if NS.lastBakeInfo and NS.lastBakeInfo.st then
  maxn = math.max(maxn, tonumber(NS.lastBakeInfo.st) or 0)
  end
  if NS.lastBakeInfo and NS.lastBakeInfo.aoe then
  maxn = math.max(maxn, tonumber(NS.lastBakeInfo.aoe) or 0)
  end
  maxn = math.max(maxn, 64)
  for i = 1, maxn do
  btn:SetAttribute("once_st_" .. i, nil)
  btn:SetAttribute("once_aoe_" .. i, nil)
  end
  btn:SetAttribute("once_target", nil)
  btn:SetAttribute("step", 1)
  btn:SetAttribute("aoe_step", 1)
  if Secure.UpdateIcon then
  Secure.UpdateIcon(btn)
  end
end

function Secure.BakeSpellLists(btn, stSteps, aoeSteps)
  if not btn or type(btn.Execute) ~= "function" then
  NS.lastBakeInfo.ok = false
  NS.lastBakeInfo.err = "no Execute"
  return false
  end
  local fallback = { { macrotext = "/startattack", once = false } }
  stSteps = stSteps or fallback
  aoeSteps = aoeSteps or stSteps
  if #stSteps == 0 then
  stSteps = fallback
  end
  if #aoeSteps == 0 then
  aoeSteps = { stSteps[1] }
  end
  local ok, err = pcall(function()
  BakeOneList(btn, "spelllist_st", "max_st", stSteps)
  BakeOneList(btn, "spelllist_aoe", "max_aoe", aoeSteps)
  end)
  NS.lastBakeInfo.st = #stSteps
  NS.lastBakeInfo.aoe = #aoeSteps
  NS.lastBakeInfo.ok = ok and true or false
  NS.lastBakeInfo.err = ok and nil or tostring(err)
  NS.lastBakeInfo.stPreview = FlatMacro(stSteps[1])
  NS.lastBakeInfo.stSteps = stSteps
  NS.lastBakeInfo.aoeSteps = aoeSteps
  return ok
end

function Secure.InstallOnClickWrap(btn)
  if not btn or type(btn.WrapScript) ~= "function" then
  NS.lastBakeInfo.err = "no WrapScript"
  return false
  end
  if btn._ssmSecureWrapped and type(SecureHandlerUnwrapScript) == "function" then
  local uok = pcall(SecureHandlerUnwrapScript, btn, "OnClick")
  if uok then
  btn._ssmSecureWrapped = false
  else
  NS.lastBakeInfo.err = "UnwrapScript failed - /reload"
  return btn._ssmSecureWrapped and true or false
  end
  end
  if btn._ssmSecureWrapped then
  return true
  end
  local ok, err = pcall(function()
  btn:WrapScript(btn, "OnClick", SECURE_ONCLICK_WRAP)
  end)
  if not ok then
  NS.lastBakeInfo.err = "WrapScript: " .. tostring(err)
  return false
  end
  btn._ssmSecureWrapped = true
  return true
end

function Secure.EnsureKeyDownRelay()
  if InCombatLockdown() then
  return BUTTON_NAME
  end
  local useDown = C_CVar and C_CVar.GetCVar and C_CVar.GetCVar("ActionButtonUseKeyDown") == "1"
  if not useDown then
  return BUTTON_NAME
  end
  local relay = _G[RELAY_NAME]
  if not relay then
  relay = CreateFrame("Button", RELAY_NAME, nil, "SecureActionButtonTemplate")
  relay.ssmKeyDownRelay = true
  relay:RegisterForClicks("AnyDown")
  relay:SetAttribute("useOnKeyDown", true)
  end
  if relay.ssmKeyDownRelay and Secure.seqButton then
  relay:SetAttribute("type", "click")
  relay:SetAttribute("clickbutton", Secure.seqButton)
  relay:SetAttribute("useOnKeyDown", true)
  end
  return RELAY_NAME
end

function Secure.GetBindingCommand()
  return "CLICK " .. Secure.EnsureKeyDownRelay() .. ":LeftButton"
end

-- Action-bar style number keys (main bar 1-0) for one-click bind UI
Secure.ACTION_BAR_KEYS = { "1", "2", "3", "4", "5", "6", "7", "8", "9", "0" }
Secure.MACRO_NAME = "SpellSmasher"

function Secure.GetBoundKey()
  for _, cmd in ipairs({
  "CLICK " .. BUTTON_NAME .. ":LeftButton",
  "CLICK " .. RELAY_NAME .. ":LeftButton",
  Secure.GetBindingCommand(),
  }) do
  local k = GetBindingKey and GetBindingKey(cmd)
  if k and k ~= "" then
  return k
  end
  end
  return nil
end

--- Main bar: key "1" > slot 1 ... key "0" > slot 10
function Secure.KeyToActionSlot(key)
  key = tostring(key or "")
  if key == "0" then
  return 10
  end
  local n = tonumber(key)
  if n and n >= 1 and n <= 9 then
  return n
  end
  return nil
end

local function ClearMashBindings()
  for _, cmd in ipairs({
  "CLICK " .. BUTTON_NAME .. ":LeftButton",
  "CLICK " .. RELAY_NAME .. ":LeftButton",
  }) do
  while true do
  local k = GetBindingKey and GetBindingKey(cmd)
  if not k or k == "" then
  break
  end
  SetBinding(k, nil)
  end
  end
end

--- Character macro that clicks the secure runner (mouse on bar uses this).
function Secure.EnsureMashMacro()
  if InCombatLockdown() then
  return 0
  end
  local name = Secure.MACRO_NAME
  -- Key-down CVar: click relay if present so bar mouse-down matches key
  local body = "#showtooltip\n/click " .. BUTTON_NAME .. " LeftButton"
  local idx = GetMacroIndexByName and GetMacroIndexByName(name) or 0
  if not idx or idx == 0 then
  -- perCharacter = true
  local ok = pcall(CreateMacro, name, "INV_MISC_QUESTIONMARK", body, 1)
  if not ok then
  ok = pcall(CreateMacro, name, "INV_MISC_QUESTIONMARK", body, true)
  end
  if not ok then
  -- account-wide fallback if character slots full
  pcall(CreateMacro, name, "INV_MISC_QUESTIONMARK", body, false)
  end
  idx = GetMacroIndexByName(name) or 0
  else
  pcall(EditMacro, idx, name, nil, body)
  end
  return idx or 0
end

--- Put SpellSmasher macro on main-bar action slot (1-12).
function Secure.PlaceMashMacroOnSlot(slot)
  slot = tonumber(slot)
  if not slot or slot < 1 or slot > 12 then
  return false, "Bad action slot."
  end
  if InCombatLockdown() then
  return false, "Can't place on action bar in combat."
  end
  local idx = Secure.EnsureMashMacro()
  if not idx or idx == 0 then
  return false, "Could not create SpellSmasher macro (macro slots full?)."
  end
  ClearCursor()
  PickupMacro(idx)
  PlaceAction(slot)
  ClearCursor()
  if NS.db then
  NS.db.barSlot = slot
  end
  return true, "Placed on action bar slot " .. tostring(slot)
end

function Secure.ClearMashFromSlot(slot)
  slot = tonumber(slot) or (NS.db and NS.db.barSlot)
  if not slot or InCombatLockdown() then
  return
  end
  local aType, aID = GetActionInfo(slot)
  if aType == "macro" and aID then
  local mname = GetMacroInfo(aID)
  if mname == Secure.MACRO_NAME then
  PickupAction(slot)
  ClearCursor()
  end
  end
end

--- Walk known action buttons (retail bar names).
local function ForEachActionButton(fn)
  local prefixes = {
  "ActionButton",
  "MultiBarBottomLeftButton",
  "MultiBarBottomRightButton",
  "MultiBarLeftButton",
  "MultiBarRightButton",
  "MultiBar5Button",
  "MultiBar6Button",
  "MultiBar7Button",
  }
  for _, prefix in ipairs(prefixes) do
  for i = 1, 12 do
  local b = _G[prefix .. i]
  if b then
  fn(b)
  end
  end
  end
end

function Secure.ActionButtonIsMash(btn)
  if not btn then
  return false
  end
  local action = btn.action
  if (not action or action == 0) and btn.GetAttribute then
  action = btn:GetAttribute("action")
  end
  if not action or action == 0 then
  return false
  end
  local aType, aID = GetActionInfo(action)
  if aType ~= "macro" then
  return false
  end
  local mname = aID and GetMacroInfo(aID)
  return mname == Secure.MACRO_NAME
end

--- Paint live next-spell icon onto every action-bar button that holds our macro.
function Secure.PaintMashActionButtons()
  local tex = Secure.iconState and Secure.iconState.tex
  if not tex then
  tex = QUESTION_ICON
  end
  local spell = Secure.iconState and Secure.iconState.spell
  ForEachActionButton(function(btn)
  if not Secure.ActionButtonIsMash(btn) then
  return
  end
  local icon = btn.icon or (btn.GetIcon and btn:GetIcon()) or btn.Icon
  if not icon and btn.GetName then
  icon = _G[btn:GetName() .. "Icon"]
  end
  if icon and icon.SetTexture then
  icon:SetTexture(tex)
  if icon.SetTexCoord then
  icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
  end
  end
  -- Cooldown swipe on bar button if present
  local cd = btn.cooldown or (btn.GetName and _G[btn:GetName() .. "Cooldown"])
  if cd and cd.SetCooldown and spell then
  local start, dur = Secure.GetSpellCooldownTimes(spell)
  if start and dur and dur > 1.4 then
  cd:SetCooldown(start, dur)
  end
  end
  end)
end

function Secure.SyncFloatingVisibility()
  local db = NS.db
  local btn = Secure.seqButton
  if not db or not btn then
  return
  end
  -- Prefer action bar: hide floating when barSlot set and hideFloatingWhenBar
  local hideForBar = db.hideFloatingWhenBar ~= false and db.barSlot ~= nil
  if hideForBar then
  db.hidden = true
  end
  -- ApplyLayout respects db.hidden
  if not InCombatLockdown() then
  Secure.ApplyLayout()
  end
end

--- Bind mash to action-bar key 1-0: place macro on that slot + key fires secure runner.
--- Live icon paints on the bar button (not the floating frame).
--- @return boolean ok, string message
function Secure.BindMashToKey(key)
  key = tostring(key or "")
  if key == "" then
  return false, "No key."
  end
  if InCombatLockdown() then
  return false, "Can't change keybinds in combat."
  end
  if not SetBinding then
  return false, "Binding API unavailable."
  end

  local slot = Secure.KeyToActionSlot(key)
  if not slot then
  return false, "Only keys 1-0 (main action bar) for bar placement."
  end

  -- Remove previous bar slot if moving
  if NS.db and NS.db.barSlot and NS.db.barSlot ~= slot then
  Secure.ClearMashFromSlot(NS.db.barSlot)
  end

  local placeOk, placeMsg = Secure.PlaceMashMacroOnSlot(slot)
  if not placeOk then
  return false, placeMsg
  end

  local conflict = GetBindingAction and GetBindingAction(key) or ""
  ClearMashBindings()

  local target = Secure.EnsureKeyDownRelay()
  local cmd = "CLICK " .. target .. ":LeftButton"
  local ok = SetBinding(key, cmd)
  if not ok then
  return false, "Could not bind key " .. key
  end

  local bindSet = GetCurrentBindingSet and GetCurrentBindingSet() or 1
  if SaveBindings then
  SaveBindings(bindSet)
  end

  if NS.db then
  NS.db.barSlot = slot
  NS.db.barKey = key
  if NS.db.hideFloatingWhenBar ~= false then
  NS.db.hidden = true
  end
  end
  Secure.SyncFloatingVisibility()
  Secure.UpdateIcon(Secure.seqButton)
  Secure.PaintMashActionButtons()

  local msg = "Mash on |cff3dd68caction bar "
  .. tostring(slot)
  .. "|r (key "
  .. key
  .. ") - icon follows next spell"
  if conflict and conflict ~= "" and conflict ~= cmd then
  msg = msg .. "  |cfff0b429(key was " .. conflict .. ")|r"
  end
  return true, msg
end

function Secure.UnbindMashKey()
  if InCombatLockdown() then
  return false, "Can't change keybinds in combat."
  end
  local had = Secure.GetBoundKey()
  local slot = NS.db and NS.db.barSlot
  ClearMashBindings()
  if slot then
  Secure.ClearMashFromSlot(slot)
  end
  local bindSet = GetCurrentBindingSet and GetCurrentBindingSet() or 1
  if SaveBindings then
  SaveBindings(bindSet)
  end
  if NS.db then
  NS.db.barSlot = nil
  NS.db.barKey = nil
  -- Show floating again so they still have a way to mash if needed
  NS.db.hidden = false
  end
  Secure.SyncFloatingVisibility()
  if had or slot then
  return true, "Cleared mash from key/bar" .. (had and (" (key " .. had .. ")") or "") .. "."
  end
  return true, "No mash keybind was set."
end

-- -- Live spell icon (show the NEXT mash spell) ----------

--- Pull spell/item display name from a baked macrotext line.
function Secure.SpellNameFromMacro(macrotext)
  macrotext = tostring(macrotext or "")
  -- Prefer last non-helper combat line
  local lastCast
  for line in (macrotext .. "\n"):gmatch("(.-)\n") do
  line = NS.Trim(line)
  local low = line:lower()
  if line ~= ""
  and not low:find("/startattack", 1, true)
  and not low:find("targetenemy", 1, true)
  and not low:find("/stopmacro", 1, true)
  then
  if low:find("^/cast") or low:find("^/use") or low:find("^/castsequence") then
  lastCast = line
  end
  end
  end
  if not lastCast then
  return nil
  end
  -- /cast [conds] Spell Name  or  /cast Spell Name
  local name = lastCast:match("%]%s*(.+)$") or lastCast:match("^/%w+%s+(.+)$")
  if not name then
  return nil
  end
  name = NS.Trim(name)
  -- castsequence is "reset=... spell, spell" - take first spell token
  if lastCast:lower():find("castsequence", 1, true) then
  name = name:match("([^,;]+)") or name
  name = name:gsub("^reset=%S+%s*", "")
  name = NS.Trim(name)
  end
  if name == "" then
  return nil
  end
  return name
end

--- Resolve display icon fileID for a spell name or id.
function Secure.GetSpellIcon(spellName)
  local icon = Secure.ResolveSpell(spellName)
  return icon and icon.iconID or nil
end

--- Full resolve: icon + spellID + display name (for list icons + GameTooltip:SetSpellByID).
--- spellIdent: number spellId, or string name (localized or English if client knows it).
function Secure.ResolveSpell(spellIdent)
  if spellIdent == nil or spellIdent == "" then
  return nil
  end
  local idNum = tonumber(spellIdent)
  -- Map English pack labels -> spellID first (name lookup is flaky cross-class)
  if not idNum and type(spellIdent) == "string" and NS.SpellIDMap and NS.SpellIDMap.GetSpellID then
  idNum = NS.SpellIDMap.GetSpellID(spellIdent)
  end
  local key = idNum or spellIdent

  local out = { iconID = nil, spellID = idNum, name = type(spellIdent) == "string" and spellIdent or nil }

  if C_Spell then
  if C_Spell.GetSpellInfo then
  local info = C_Spell.GetSpellInfo(key)
  if type(info) == "table" then
  out.iconID = info.iconID or info.originalIconID or out.iconID
  out.spellID = info.spellID or out.spellID
  out.name = info.name or out.name
  out.castTime = info.castTime
  out.minRange = info.minRange
  out.maxRange = info.maxRange
  end
  end
  if not out.iconID and C_Spell.GetSpellTexture then
  -- Returns fileID(s); first non-nil wins
  local a, b, c = C_Spell.GetSpellTexture(key)
  out.iconID = a or b or c
  end
  if not out.spellID and C_Spell.GetSpellIDForSpellIdentifier then
  local sid = C_Spell.GetSpellIDForSpellIdentifier(key)
  if sid then
  out.spellID = sid
  end
  end
  end

  if not out.iconID and GetSpellTexture then
  out.iconID = GetSpellTexture(key)
  end

  -- Catalog texture path fallback (Interface\Icons\slug)
  if not out.iconID and type(spellIdent) == "string" and NS.SpellIDMap and NS.SpellIDMap.GetTexture then
  local tex, mid = NS.SpellIDMap.GetTexture(spellIdent)
  if tex then
  out.iconID = tex
  out.spellID = out.spellID or mid
  end
  end

  -- Item fallback (rare /use lines)
  if not out.iconID and C_Item and C_Item.GetItemIconByID and idNum then
  out.iconID = C_Item.GetItemIconByID(idNum)
  end

  if not out.iconID and not out.spellID and not out.name then
  return nil
  end
  return out
end

--- Resolve step label + macro into icon/spell for UI rows.
--- Prefer stamped spellId, then SpellIDMap (name->id, class/spec-aware), then live C_Spell.
--- opts: optional { className=, spec= } from the pack (needed for dual IDs like Kill Shot).
function Secure.ResolveStepSpell(step, opts)
  if type(step) ~= "table" then
  return nil
  end
  opts = opts or {}
  local className = opts.className or step.className
  local spec = opts.spec or step.spec
  local nameGuess = NS.Trim(step.label or step.name or "")
  if nameGuess:find("/", 1, true) then
  nameGuess = NS.Trim(nameGuess:match("^([^/]+)") or nameGuess)
  end
  if nameGuess:find("+", 1, true) then
  nameGuess = NS.Trim(nameGuess:match("^([^+]+)") or nameGuess)
  end

  local r
  -- Spec-aware catalog ID wins over a stamped generic collision (e.g. Kill Shot SV vs BM/MM)
  local mapIdPreferred = nil
  if nameGuess ~= "" and NS.SpellIDMap and NS.SpellIDMap.GetSpellID then
  mapIdPreferred = NS.SpellIDMap.GetSpellID(nameGuess, className, spec)
  end
  local stamped = mapIdPreferred or tonumber(step.spellId or step.spellID)
  if stamped then
  r = Secure.ResolveSpell(stamped)
  end

  -- Catalog map (English pack labels) — reliable icons for other-class packs
  if (not r or not r.iconID) and nameGuess ~= "" and NS.SpellIDMap then
  local mapId = mapIdPreferred or (NS.SpellIDMap.GetSpellID and NS.SpellIDMap.GetSpellID(nameGuess, className, spec))
  if mapId then
  r = Secure.ResolveSpell(mapId) or r
  if r then
  r.spellID = r.spellID or mapId
  end
  end
  if (not r or not r.iconID) and NS.SpellIDMap.GetTexture then
  local tex, mid = NS.SpellIDMap.GetTexture(nameGuess, className, spec)
  if tex then
  r = r or {}
  r.iconID = tex
  r.spellID = r.spellID or mid
  r.name = r.name or nameGuess
  end
  end
  end

  if (not r or not r.iconID) and nameGuess ~= "" then
  r = Secure.ResolveSpell(nameGuess) or r
  end
  if (not r or not r.iconID) and step.macro and step.macro ~= "" then
  local fromMac = Secure.SpellNameFromMacro(step.macro)
  if fromMac then
  if NS.SpellIDMap and NS.SpellIDMap.GetSpellID then
  local mid = NS.SpellIDMap.GetSpellID(fromMac, className, spec)
  if mid then
  r = Secure.ResolveSpell(mid) or r
  end
  end
  if not r or not r.iconID then
  local r2 = Secure.ResolveSpell(fromMac)
  if r2 then
  r = r2
  end
  end
  end
  end
  return r
end

function Secure.GetSpellCooldownTimes(spellName)
  if not spellName or spellName == "" then
  return 0, 0
  end
  if C_Spell and C_Spell.GetSpellCooldownInfo then
  local info = C_Spell.GetSpellCooldownInfo(spellName)
  if type(info) == "table" then
  local start = info.startTime or info.start or 0
  local dur = info.duration or 0
  if start and dur and dur > 0 then
  return start, dur
  end
  end
  end
  if GetSpellCooldown then
  local start, dur = GetSpellCooldown(spellName)
  if start and dur and dur > 0 then
  return start, dur
  end
  end
  return 0, 0
end

--- Icon = spell for the step index the NEXT press will fire.
function Secure.UpdateIcon(btn)
  btn = btn or Secure.seqButton
  if not btn or not btn.icon then
  return
  end
  local bi = NS.lastBakeInfo
  local steps = bi and bi.stSteps
  local maxn = steps and #steps or 0
  local step = tonumber(btn:GetAttribute("step")) or 1
  if step < 1 then
  step = 1
  end
  if maxn > 0 and step > maxn then
  step = 1
  end

  local macro = ""
  if maxn > 0 and steps[step] then
  macro = FlatMacro(steps[step])
  else
  macro = tostring(btn:GetAttribute("macrotext") or "")
  end

  local spell = Secure.SpellNameFromMacro(macro)
  local tex = spell and Secure.GetSpellIcon(spell) or nil
  if not tex then
  tex = QUESTION_ICON
  end

  btn.icon:SetTexture(tex)
  if btn.icon:GetNumPoints() == 0 then
  btn.icon:SetAllPoints()
  end
  -- Slight darken so green border still reads
  btn.icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
  btn.icon:Show()

  if btn.spellLabel then
  if spell and spell ~= "" then
  local short = spell
  if #short > 11 then
  short = short:sub(1, 10) .. "..."
  end
  btn.spellLabel:SetText(short)
  btn.spellLabel:Show()
  else
  btn.spellLabel:SetText("SSM")
  btn.spellLabel:Show()
  end
  end

  -- Hide the old multi-line pack label when we have a real spell icon
  if btn.label then
  if spell and tex and tex ~= QUESTION_ICON then
  btn.label:Hide()
  else
  btn.label:Show()
  end
  end

  -- Cooldown swipe for the NEXT spell if it's on CD (honest feedback)
  if btn.cooldown then
  local start, dur = Secure.GetSpellCooldownTimes(spell)
  if start and dur and dur > 1.4 then
  btn.cooldown:SetCooldown(start, dur)
  else
  btn.cooldown:Clear()
  end
  end

  Secure.iconState.spell = spell
  Secure.iconState.step = step
  Secure.iconState.tex = tex
  Secure.iconState.total = maxn

  -- Always paint action-bar slots that hold our macro (main goal)
  Secure.PaintMashActionButtons()
end

function Secure.InstallIconHooks(btn)
  if not btn or btn._ssmIconHooked then
  return
  end
  btn._ssmIconHooked = true
  -- Secure wrap updates step/macrotext  >  refresh picture (works in combat)
  btn:HookScript("OnAttributeChanged", function(self, name)
  if name == "step" or name == "macrotext" or name == "aoe_step" then
  Secure.UpdateIcon(self)
  end
  end)
  -- After click (insecure) - belt and suspenders
  btn:HookScript("OnClick", function(self)
  Secure.UpdateIcon(self)
  end)
  -- Keep bar icon alive (Blizzard action-button updates overwrite textures)
  if not Secure._iconTicker then
  Secure._iconTicker = C_Timer.NewTicker(0.15, function()
  if Secure.seqButton then
  Secure.UpdateIcon(Secure.seqButton)
  elseif NS.db and NS.db.barSlot then
  Secure.PaintMashActionButtons()
  end
  end)
  end
  -- Re-paint after default action button updates (retail mixin)
  if not Secure._actionHooked then
  Secure._actionHooked = true
  if ActionBarActionButtonMixin and ActionBarActionButtonMixin.Update then
  hooksecurefunc(ActionBarActionButtonMixin, "Update", function(self)
  if Secure.ActionButtonIsMash(self) then
  Secure.PaintMashActionButtons()
  end
  end)
  end
  if ActionBarActionButtonMixin and ActionBarActionButtonMixin.UpdateIcon then
  hooksecurefunc(ActionBarActionButtonMixin, "UpdateIcon", function(self)
  if Secure.ActionButtonIsMash(self) then
  Secure.PaintMashActionButtons()
  end
  end)
  end
  end
end

function Secure.Recompile()
  local btn = Secure.seqButton
  local db = NS.db
  if not btn or not db then
  return
  end
  if InCombatLockdown() then
  NS.pendingRecompile = true
  return
  end
  NS.pendingRecompile = false

  local seqs = db.sequences or {}
  local st = seqs[db.activeIndex or 1] or seqs[1]
  -- One-button product: same compiled list for mash + optional Shift list.
  -- No separate ST/AoE packs. aoeIndex ignored unless user sets a second sequence later.
  local aoe = st
  if db.aoeIndex and seqs[db.aoeIndex] and db.aoeIndex ~= (db.activeIndex or 1) then
  aoe = seqs[db.aoeIndex]
  end

  -- Defensive helper auto-off if that spell is already a sequence step
  if db.helperDefensive and st then
  local className = NS.Trim(st.className or "") ~= "" and NS.Trim(st.className) or NS.ActiveClassName()
  local base = className and NS.DEFENSIVE_BY_CLASS[className]
  if base and NS.Compile.SequenceHasSpellLabel(st, base) then
  db.helperDefensive = false
  NS.Print("|cfff0b429Defensive helper Off|r - " .. base .. " is already on the sequence.")
  end
  end

  local stSteps, stStats = NS.Compile.CompileFlatSteps(st)
  local aoeSteps, aoeStats = NS.Compile.CompileFlatSteps(aoe)
  local fallback = { { macrotext = "/startattack", once = false } }
  stSteps = stSteps or fallback
  aoeSteps = aoeSteps or stSteps
  if #stSteps == 0 then
  stSteps = fallback
  end
  if #aoeSteps == 0 then
  aoeSteps = stSteps
  end
  NS.lastBakeInfo.compile = stStats
  NS.lastBakeInfo.order = stStats and stStats.order or {}
  NS.lastBakeInfo.groupSummary = stStats and stStats.groupSummary or {}

  local baked = Secure.BakeSpellLists(btn, stSteps, aoeSteps)
  local wrapped = Secure.InstallOnClickWrap(btn)
  Secure.EnsureKeyDownRelay()

  local first = FlatMacro(stSteps[1])
  if first == "" then
  first = "/startattack"
  end
  btn:SetAttribute("type", "macro")
  btn:SetAttribute("macro", nil)
  btn:SetAttribute("unit", nil)
  btn:SetAttribute("macrotext", first)
  btn:SetAttribute("step", 1)
  btn:SetAttribute("aoe_step", 1)
  -- Fresh compile = new Once engagement
  Secure.ResetOnceEngagement("recompile")
  btn:SetAttribute("useOnKeyDown", false)
  local sm = tostring(db.shiftMode or "ignore"):lower()
  if sm ~= "aoe" and sm ~= "ignore" and sm ~= "pause" then
  sm = "ignore"
  end
  btn:SetAttribute("shiftmode", sm)
  btn:RegisterForClicks("AnyUp")

  local name = st and st.name or "empty"
  if btn.label then
  local n = (st and st.steps and #st.steps) or 0
  local w = #stSteps
  btn.label:SetText("SSM\n" .. tostring(name):sub(1, 12) .. (n > 0 and ("\n" .. n .. " > " .. w) or ""))
  end

  Secure.InstallIconHooks(btn)
  Secure.UpdateIcon(btn)
  -- Re-assert macro on saved bar slot after recompile
  if NS.db and NS.db.barSlot and not InCombatLockdown() then
  local aType = GetActionInfo(NS.db.barSlot)
  if aType ~= "macro" then
  Secure.PlaceMashMacroOnSlot(NS.db.barSlot)
  else
  local _, aID = GetActionInfo(NS.db.barSlot)
  local mname = aID and GetMacroInfo(aID)
  if mname ~= Secure.MACRO_NAME then
  Secure.PlaceMashMacroOnSlot(NS.db.barSlot)
  end
  end
  Secure.PaintMashActionButtons()
  Secure.SyncFloatingVisibility()
  end

  -- Quiet by default: leave-combat / AA toggles used to spam chat with
  -- "Compiled ..." and "Next cast ..." on every recompile. Only announce when
  -- Secure.verboseCompile is set (e.g. /ssm recompile).
  if not baked or not wrapped then
  NS.Print("|cffff6b7aSecure bake failed:|r " .. tostring(NS.lastBakeInfo.err or "unknown"))
  elseif Secure.verboseCompile and stStats and stStats.total and stStats.total > 0 then
  NS.Print(
  "Compiled |cff3dd68c"
  .. tostring(stStats.total)
  .. "|r clicks  -  once="
  .. tostring(stStats.once)
  .. " seq="
  .. tostring(stStats.seq)
  .. " pri="
  .. tostring(stStats.pri)
  .. "  -  Shift="
  .. sm
  )
  local sp = Secure.iconState.spell
  if sp then
  if NS.db and NS.db.barSlot then
  NS.Print(
  "Next cast: |cff3dd68c"
  .. sp
  .. "|r  -  bar "
  .. tostring(NS.db.barSlot)
  )
  else
  NS.Print("Next cast: |cff3dd68c" .. sp .. "|r")
  end
  end
  end
  Secure.verboseCompile = false
end

function Secure.ApplyLayout()
  local btn = Secure.seqButton
  local db = NS.db
  if not btn or not db then
  return
  end
  if InCombatLockdown() then
  NS.pendingLayout = true
  return
  end
  NS.pendingLayout = false
  btn:ClearAllPoints()
  btn:SetPoint(db.point or "CENTER", UIParent, db.relPoint or "CENTER", db.x or 0, db.y or 200)
  btn:SetScale(db.scale or 1)
  if db.hidden then
  btn:Hide()
  else
  btn:Show()
  end
  if db.locked then
  btn:SetMovable(false)
  btn:RegisterForDrag()
  else
  btn:SetMovable(true)
  btn:RegisterForDrag("LeftButton")
  end
  btn:EnableMouse(true)
end

function Secure.CreateButton()
  if Secure.seqButton then
  return Secure.seqButton
  end
  local C = NS.C
  local btn = CreateFrame("Button", BUTTON_NAME, UIParent, "SecureActionButtonTemplate,SecureHandlerBaseTemplate")
  Secure.seqButton = btn
  btn:SetSize(58, 58)
  btn:SetFrameStrata("HIGH")
  btn:RegisterForClicks("AnyUp")
  btn:SetAttribute("useOnKeyDown", false)
  btn:SetAttribute("type", "macro")
  btn:SetAttribute("macrotext", "/startattack")
  btn:SetAttribute("step", 1)
  btn:SetAttribute("aoe_step", 1)
  btn:SetAttribute("name", BUTTON_NAME)

  -- Spell icon (live - updates each mash)
  local icon = btn:CreateTexture(nil, "ARTWORK")
  icon:SetPoint("TOPLEFT", 3, -3)
  icon:SetPoint("BOTTOMRIGHT", -3, 3)
  icon:SetTexture(QUESTION_ICON)
  icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
  btn.icon = icon

  local cd = CreateFrame("Cooldown", nil, btn, "CooldownFrameTemplate")
  cd:SetPoint("TOPLEFT", icon, "TOPLEFT", 0, 0)
  cd:SetPoint("BOTTOMRIGHT", icon, "BOTTOMRIGHT", 0, 0)
  cd:SetDrawEdge(false)
  btn.cooldown = cd

  -- Border chrome above icon edge
  local chrome = CreateFrame("Frame", nil, btn, "BackdropTemplate")
  chrome:SetAllPoints()
  chrome:SetFrameLevel(btn:GetFrameLevel() + 2)
  NS.HexBackdrop(chrome, { 0, 0, 0, 0.01 }, C.green, 12, "panel")
  btn.chrome = chrome
  -- Transparent fill so icon shows through
  if chrome.SetBackdropColor then
  chrome:SetBackdropColor(0, 0, 0, 0)
  end

  -- Spell name strip at bottom of button
  local nameBg = chrome:CreateTexture(nil, "ARTWORK")
  nameBg:SetPoint("BOTTOMLEFT", 2, 2)
  nameBg:SetPoint("BOTTOMRIGHT", -2, 2)
  nameBg:SetHeight(14)
  nameBg:SetColorTexture(0, 0, 0, 0.72)
  btn.nameBg = nameBg

  local spellLabel = chrome:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
  spellLabel:SetPoint("BOTTOM", 0, 4)
  spellLabel:SetJustifyH("CENTER")
  spellLabel:SetWidth(52)
  spellLabel:SetText("SSM")
  NS.SetFS(spellLabel, C.text)
  btn.spellLabel = spellLabel

  -- Fallback multi-line label (pack name) if no spell resolved
  local label = chrome:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
  label:SetPoint("CENTER", 0, 4)
  NS.SetFS(label, C.text)
  label:SetText("SSM")
  label:Hide()
  btn.label = label

  if not InCombatLockdown() then
  Secure.BakeSpellLists(btn, { "/startattack" }, { "/startattack" })
  Secure.InstallOnClickWrap(btn)
  Secure.EnsureKeyDownRelay()
  Secure.InstallIconHooks(btn)
  Secure.UpdateIcon(btn)
  end

  btn:SetScript("OnDragStart", function(self)
  if NS.db and not NS.db.locked and not InCombatLockdown() then
  self:StartMoving()
  end
  end)
  btn:SetScript("OnDragStop", function(self)
  self:StopMovingOrSizing()
  if not NS.db then
  return
  end
  local p, _, rp, x, y = self:GetPoint(1)
  NS.db.point, NS.db.relPoint, NS.db.x, NS.db.y = p, rp, x, y
  end)
  btn:SetScript("OnEnter", function(self)
  if self.chrome then
  NS.HexBackdrop(self.chrome, { 0, 0, 0, 0 }, C.accent, 12, "panel")
  end
  Secure.UpdateIcon(self)
  GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
  GameTooltip:AddLine("SpellSmasher " .. NS.VERSION, C.green[1], C.green[2], C.green[3])
  local st = Secure.iconState
  if st.spell then
  GameTooltip:AddLine("Next: " .. st.spell, 1, 1, 1)
  GameTooltip:AddLine(
  "Step " .. tostring(st.step) .. " / " .. tostring(st.total or "?"),
  C.muted[1],
  C.muted[2],
  C.muted[3]
  )
  -- Spell tooltip if API allows
  if GameTooltip.SetSpellByID and C_Spell and C_Spell.GetSpellIDForSpellIdentifier then
  local id = C_Spell.GetSpellIDForSpellIdentifier(st.spell)
  if id then
  GameTooltip:AddLine(" ")
  -- keep short; full spell tip can be heavy
  GameTooltip:AddLine("Spell ID " .. tostring(id), C.muted[1], C.muted[2], C.muted[3])
  end
  end
  else
  GameTooltip:AddLine("One press = next list spell", 1, 1, 1)
  end
  GameTooltip:AddLine("Icon updates every mash (next cast)", C.muted[1], C.muted[2], C.muted[3])
  local k = Secure.GetBoundKey()
  if k then
  GameTooltip:AddLine("Keybind: " .. k, C.gold[1], C.gold[2], C.gold[3])
  else
  GameTooltip:AddLine("No keybind - /ssm  >  Mash key 1-0", C.danger[1], C.danger[2], C.danger[3])
  end
  GameTooltip:Show()
  end)
  btn:SetScript("OnLeave", function(self)
  if self.chrome then
  NS.HexBackdrop(self.chrome, { 0, 0, 0, 0 }, C.green, 12, "panel")
  end
  GameTooltip:Hide()
  end)
  return btn
end

function Secure.DumpSample(maxLines)
  maxLines = maxLines or 12
  local bi = NS.lastBakeInfo
  if not bi or not bi.stSteps then
  NS.Print("No bake yet - import/create a pack and recompile.")
  return
  end
  NS.Print("=== /ssm secure (sample ST macros) v" .. NS.VERSION .. " ===")
  NS.Print(
  "steps="
  .. tostring(bi.st)
  .. " ok="
  .. tostring(bi.ok)
  .. " mode="
  .. tostring(bi.compile and bi.compile.priorityMode or "?")
  )
  for i = 1, math.min(maxLines, #bi.stSteps) do
  local entry = bi.stSteps[i]
  local mt = FlatMacro(entry):gsub("\n", " | ")
  local tag = FlatIsOnce(entry) and " [once]" or ""
  NS.Print(string.format("%02d  %s%s", i, mt, tag))
  end
  if #bi.stSteps > maxLines then
  NS.Print("... " .. (#bi.stSteps - maxLines) .. " more")
  end
end
