--[[
  CastGuard — only castable (/cast) or usable (/use) abilities on mash lists.

  Product: if it cannot be cast/used, it is not a sequence step.
  Pure talent passives never invent a fake /cast.

  Mirrors theme assets/cast-guard.js (keep patterns in sync).
]]

local NS = SpellSmasherNS
local CG = {}
NS.CastGuard = CG

local function Trim(s)
  return (tostring(s or ""):gsub("^%s+", ""):gsub("%s+$", ""))
end

local function Norm(s)
  s = string.lower(tostring(s or ""))
  s = s:gsub("['']", "")
  s = s:gsub("[^%w%+]+", " ")
  s = s:gsub("%s+", " ")
  return Trim(s)
end

-- Passive talent / non-button name patterns (lowercase match on raw or norm).
local PASSIVE_PATTERNS = {
  "^improved ",
  "^mastery ",
  "^mastery:",
  "knowledge",
  "teachings",
  "proliferat",
  "overflowing",
  "energized",
  "passive",
  "percent",
  "%%",
  "^fleet footed",
  "^deadened nerves",
  "^nightstalker$",
  "^subterfuge$",
  "^vigor$",
  "^cheat death",
  "^elusiveness$",
  "^thick hide",
  "^oakskin$",
  "^forestwalk",
  "^primal fury$",
  "^fluid form",
  "^ursine vigor",
  "^well%-honed",
  "^lycara",
  "^aessina",
  "^incisive blade",
  "^keen engagement",
  "^student of suffering",
  "^enduring torment",
  "^set fire to the pain",
  "^focused hatred",
  "^thrill of the fight",
  "^charred warblades",
  "^master of the glaive",
  "^chase the prey",
  "^rush of chaos",
  "^quickened sigils",
  "^flames of fury",
  "^internal struggle",
  "^collective anguish",
  "^erratic felheart",
  "^long night$",
  "^pitch black",
  "^first of the illidari",
  "^illidari knowledge",
  "^precise sigils",
  "^disrupting fury",
  "^unhindered assault",
  "^lost in darkness",
  "^soul rending",
  "^infernal armor",
  "^aldrachi design",
  "^feed the demon",
  "^fallout$",
  "^fiery demise",
  "^charred flesh",
  "^void reaver",
  "^calcified",
  "^ascending flame",
  "^burning blood",
  "^soul furnace",
  "^illuminated sigils",
  "^cycle of binding",
  "^stoke the flames",
  "^focused cleave",
  "^agonizing flames",
  "^roaring fire",
  "^animosity$",
  "^font of magic",
  "^aura of ",
  "^will of ",
  "^tome of ",
  "^flow of ",
  " design$",
}

local function isCustomLabel(label)
  return string.find(string.lower(tostring(label or "")), "custom%s*cast") ~= nil
end

local function isGearUseLabel(label)
  local s = tostring(label or "")
  if string.find(s, "^[Uu]se%s+[Tt]op%s+[Tt]rinket") then return true end
  if string.find(s, "^[Uu]se%s+[Bb]ottom%s+[Tt]rinket") then return true end
  if string.find(s, "^[Uu]se%s+[Bb]oots") then return true end
  if string.find(s, "^[Uu]se%s+[Bb]elt") then return true end
  if string.find(s, "^[Uu]se%s+[Gg]loves") then return true end
  if string.find(s, "^[Uu]se%s+[Cc]loak") then return true end
  if string.find(s, "^[Uu]se%s+[Mm]ain%s+[Hh]and") then return true end
  if string.find(s, "^[Uu]se%s+[Oo]ff%s+[Hh]and") then return true end
  if string.find(s, "^[Uu]se%s+[Hh]ealthstone") then return true end
  return false
end

function CG.IsPassiveSpellName(label)
  local s = Trim(label)
  if s == "" then
    return true
  end
  if isCustomLabel(s) or isGearUseLabel(s) then
    return false
  end
  -- Combo "A + B"
  if string.find(s, "+", 1, true) then
    local allPassive = true
    local any = false
    for part in string.gmatch(s, "[^%+]+") do
      part = Trim(part)
      if part ~= "" then
        any = true
        if not CG.IsPassiveSpellName(part) then
          allPassive = false
          break
        end
      end
    end
    return any and allPassive
  end
  local low = string.lower(s)
  local nk = Norm(s)
  for _, pat in ipairs(PASSIVE_PATTERNS) do
    if string.find(low, pat) or string.find(nk, pat) then
      return true
    end
  end
  return false
end

function CG.MacroHasCombatLine(macro)
  local raw = tostring(macro or "")
  if Trim(raw) == "" then
    return false
  end
  for line in (raw .. "\n"):gmatch("(.-)\n") do
    line = Trim(line)
    if line ~= "" and not string.find(line, "^#") then
      local low = string.lower(line)
      if string.find(low, "^/castsequence") then
        return true
      end
      if string.find(low, "^/cast") or string.find(low, "^/use") then
        -- Need something after the verb (spell name or [conds] + name)
        local rest = line:match("^/%S+%s*(.*)$") or ""
        rest = Trim(rest)
        if rest ~= "" then
          -- Reject bare /cast [nochanneling] with no spell token
          local afterBrackets = rest:match("^%[[^%]]*%]%s*(.*)$")
          if afterBrackets ~= nil then
            if Trim(afterBrackets) ~= "" then
              return true
            end
          else
            return true
          end
        end
      end
    end
  end
  return false
end

function CG.CombatMacroFor(label, existingMacro)
  local s = Trim(label)
  if s == "" then
    return nil
  end
  if isCustomLabel(s) then
    local m = Trim(existingMacro or "")
    if m ~= "" then
      return m
    end
    return "/cast [nochanneling] SPELL"
  end
  if CG.IsPassiveSpellName(s) then
    return nil
  end
  local existing = Trim(existingMacro or "")
  if existing ~= "" and CG.MacroHasCombatLine(existing) then
    return existing
  end
  if isGearUseLabel(s) then
    if existing ~= "" and CG.MacroHasCombatLine(existing) then
      return existing
    end
    return nil
  end
  if string.find(s, "[Ss]igil of ") or string.find(s, "[Dd]ecree") then
    return "/cast [nochanneling,@player] " .. s
  end
  return "/cast [nochanneling] " .. s
end

function CG.IsSequenceable(label, macro)
  local s = Trim(label)
  if s == "" then
    return false
  end
  if isCustomLabel(s) then
    return true
  end
  if CG.IsPassiveSpellName(s) then
    return false
  end
  local m = tostring(macro or "")
  if m ~= "" and CG.MacroHasCombatLine(m) then
    return not CG.IsPassiveSpellName(s)
  end
  return CG.CombatMacroFor(s, "") ~= nil
end

--- Filter steps table in-place-safe (returns new list). Also fills missing macros.
--- @return steps, droppedLabels[]
function CG.FilterSteps(steps)
  local kept = {}
  local dropped = {}
  if type(steps) ~= "table" then
    return kept, dropped
  end
  for _, st in ipairs(steps) do
    if type(st) == "table" then
      local lab = Trim(st.label)
      local mac = tostring(st.macro or "")
      if CG.IsSequenceable(lab, mac) then
        if not CG.MacroHasCombatLine(mac) and not isCustomLabel(lab) then
          local fixed = CG.CombatMacroFor(lab, mac)
          if fixed then
            local copy = {}
            for k, v in pairs(st) do
              copy[k] = v
            end
            copy.macro = fixed
            kept[#kept + 1] = copy
          else
            dropped[#dropped + 1] = lab ~= "" and lab or "(unnamed)"
          end
        else
          kept[#kept + 1] = st
        end
      else
        dropped[#dropped + 1] = lab ~= "" and lab or "(passive/non-cast)"
      end
    end
  end
  return kept, dropped
end
