--[[
  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.

  Layers:
  1) Exact English name blocklist (talent passives that look like real names)
  2) Pattern list (Mastery:, Improved, Design, etc.)
  3) Live client: C_Spell.IsSpellPassive(spellId) when ID is known

  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

--- Exact-name passives (English). Never sequence steps. Expand as needed.
local EXACT_PASSIVE_NAMES = {
  ["feed the demon"] = true,
  ["fiery demise"] = true,
  ["soul rending"] = true,
  ["aldrachi design"] = true,
  ["incisive blade"] = true,
  ["keen engagement"] = true,
  ["charred warblades"] = true,
  ["master of the glaive"] = true,
  ["chase the prey"] = true,
  ["rush of chaos"] = true,
  ["quickened sigils"] = true,
  ["flames of fury"] = true,
  ["internal struggle"] = true,
  ["collective anguish"] = true,
  ["erratic felheart"] = true,
  ["illidari knowledge"] = true,
  ["precise sigils"] = true,
  ["disrupting fury"] = true,
  ["unhindered assault"] = true,
  ["lost in darkness"] = true,
  ["infernal armor"] = true,
  ["void reaver"] = true,
  ["calcified spikes"] = true,
  ["ascending flame"] = true,
  ["soul furnace"] = true,
  ["illuminated sigils"] = true,
  ["cycle of binding"] = true,
  ["stoke the flames"] = true,
  ["focused cleave"] = true,
  ["agonizing flames"] = true,
  ["roaring fire"] = true,
  ["animosity"] = true,
  ["font of magic"] = true,
  ["student of suffering"] = true,
  ["enduring torment"] = true,
  ["set fire to the pain"] = true,
  ["focused hatred"] = true,
  ["thrill of the fight"] = true,
  ["long night"] = true,
  ["pitch black"] = true,
  ["first of the illidari"] = true,
  ["fallout"] = true,
  ["charred flesh"] = true,
  ["burning blood"] = true,
  ["fleet footed"] = true,
  ["deadened nerves"] = true,
  ["nightstalker"] = true,
  ["subterfuge"] = true,
  ["elusiveness"] = true,
  ["thick hide"] = true,
  ["primal fury"] = true,
  ["fluid form"] = true,
  ["ursine vigor"] = true,
  ["well-honed instincts"] = true,
  ["airborne agility"] = true,
}

-- 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

--- True if this spell ID is a pure passive (client API). Safe when offline/unknown.
function CG.IsPassiveSpellId(spellId)
  local id = tonumber(spellId)
  if not id or id <= 0 then
    return false
  end
  if C_Spell and C_Spell.IsSpellPassive then
    local ok, result = pcall(C_Spell.IsSpellPassive, id)
    if ok and result == true then
      return true
    end
  end
  if IsPassiveSpell then
    local ok, result = pcall(IsPassiveSpell, id)
    if ok and result == true then
      return true
    end
  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)
  if EXACT_PASSIVE_NAMES[low] or EXACT_PASSIVE_NAMES[nk] then
    return true
  end
  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

--- Resolve a spell ID for guard checks (step stamp → SpellIDMap → nil).
local function resolveStepSpellId(label, spellId, className, spec)
  local id = tonumber(spellId)
  if id and id > 0 then
    return id
  end
  if NS.SpellIDMap and NS.SpellIDMap.GetSpellID then
    return NS.SpellIDMap.GetSpellID(label, className, spec)
  end
  return nil
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

--- className/spec optional — used to resolve spell IDs for passive API checks.
function CG.IsSequenceable(label, macro, spellId, className, spec)
  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 id = resolveStepSpellId(s, spellId, className, spec)
  -- Only drop when the *name* is a known passive OR the ID is passive *and*
  -- we cannot invent a cast (name already cleared pattern list). If the map
  -- stamped a wrong passive ID onto a real cast name, clear ID later — do not
  -- drop the castable name solely for a bad stamp.
  if id and CG.IsPassiveSpellId(id) and 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.
--- Drops pure passives; clears passive-only spellId stamps on castable names.
--- @return steps, droppedLabels[]
function CG.FilterSteps(steps, className, spec)
  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 or st.name)
      local mac = tostring(st.macro or "")
      local sid = tonumber(st.spellId or st.spellID)
      if CG.IsSequenceable(lab, mac, sid, className, spec) then
        local copy = nil
        local function ensureCopy()
          if not copy then
            copy = {}
            for k, v in pairs(st) do
              copy[k] = v
            end
          end
          return copy
        end
        -- Bad stamp: passive ID on a castable name → re-resolve or strip
        if sid and CG.IsPassiveSpellId(sid) and not CG.IsPassiveSpellName(lab) then
          local better = nil
          if NS.SpellIDMap and NS.SpellIDMap.GetSpellID then
            better = NS.SpellIDMap.GetSpellID(lab, className, spec)
          end
          ensureCopy()
          if better and better ~= sid and not CG.IsPassiveSpellId(better) then
            copy.spellId = better
          else
            copy.spellId = nil
            copy.spellID = nil
          end
        end
        if not CG.MacroHasCombatLine(mac) and not isCustomLabel(lab) then
          local fixed = CG.CombatMacroFor(lab, mac)
          if fixed then
            ensureCopy()
            copy.macro = fixed
            kept[#kept + 1] = copy
          else
            dropped[#dropped + 1] = lab ~= "" and lab or "(unnamed)"
          end
        else
          kept[#kept + 1] = copy or st
        end
      else
        dropped[#dropped + 1] = lab ~= "" and lab or "(passive/non-cast)"
      end
    end
  end
  return kept, dropped
end
