--[[
  SpellSmasher rebuild engine - same behavior as the website theme:
  Method.gg orders + Method talents, or SpellSmasher Engine orders + Method talents.
  Product default: ONE pack from dedicated aoeSteps (M+ / multi-target order).
  No dual ST+AoE. Want a boss/ST list? Edit the steps or rebuild with role override later.
  Data: MethodOrders.lua + EngineOrders.lua (ported from site JS).
]]

local Rebuild = {}

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

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

local function MacroForStep(t)
  local name = t.name or t.label or "Spell"
  -- [nochanneling] on every combat cast: mash will not interrupt a long channel
  -- (Fel Dev / Eye Beam / etc.). Compile still re-normalizes in SpellSmasher.lua.
  if t.player then
  return "/cast [nochanneling,@player] " .. name
  end
  if t.cursor then
  return "/cast [nochanneling,@cursor] " .. name
  end
  if t.focus then
  return "/cast [nochanneling,@focus] " .. name
  end
  if t.macro and t.macro ~= "" then
  return tostring(t.macro)
  end
  return "/cast [nochanneling] " .. name
end

local function GetMethodOrder(className, spec, hero)
  if SpellSmasherMethodOrders and SpellSmasherMethodOrders.GetOrder then
  return SpellSmasherMethodOrders.GetOrder(className, spec, hero)
  end
  return nil
end

local function GetEngineOrder(className, spec, hero)
  if SpellSmasherEngineOrders and SpellSmasherEngineOrders.GetOrder then
  return SpellSmasherEngineOrders.GetOrder(className, spec, hero)
  end
  return nil
end

--- Like site PriorityOrders.getOrder(class, spec, source [, hero])
function Rebuild.GetOrder(className, spec, source, hero)
  source = string.lower(tostring(source or "method"))
  hero = Trim(hero)
  local useEngine = (source == "engine" or source == "spellsmasher" or source == "ss")
  if useEngine then
  local eng = GetEngineOrder(className, spec, hero)
  if eng then
  -- Engine mash order + Method talent package (site injectMethodLoadout)
  local method = GetMethodOrder(className, spec, hero)
  local out = {}
  for k, v in pairs(eng) do
  out[k] = v
  end
  if method and method.talents then
  out.talents = method.talents
  end
  out.source = "SpellSmasher Engine"
  out.engine = true
  out.patch = out.patch or "Midnight 12.0.7"
  if hero ~= "" then
  out.heroTalent = hero
  end
  return out
  end
  end
  local m = GetMethodOrder(className, spec, hero)
  if m then
  local out = {}
  for k, v in pairs(m) do
  out[k] = v
  end
  out.source = out.source or "Method.gg"
  out.patch = out.patch or "Midnight 12.0.7"
  if hero ~= "" then
  out.heroTalent = hero
  end
  return out
  end
  return nil
end

--- List hero talent paths for class|spec (registry + order data).
function Rebuild.ListHeroes(className, spec)
  if SpellSmasherHeroPaths and SpellSmasherHeroPaths.ListHeroes then
  local t = SpellSmasherHeroPaths.ListHeroes(className, spec)
  if t and #t > 0 then
  return t
  end
  end
  if SpellSmasherMethodOrders and SpellSmasherMethodOrders.ListHeroes then
  return SpellSmasherMethodOrders.ListHeroes(className, spec) or {}
  end
  return {}
end

function Rebuild.DefaultHero(className, spec)
  if SpellSmasherHeroPaths and SpellSmasherHeroPaths.DefaultHero then
  local h = SpellSmasherHeroPaths.DefaultHero(className, spec)
  if h and h ~= "" then
  return h
  end
  end
  local t = Rebuild.ListHeroes(className, spec)
  return t[1] or ""
end

--- Like site PriorityOrders.rebuildSequence
--- @return table { ok, steps, name, talents, usedAoeSteps, ... }
function Rebuild.RebuildSequence(className, spec, currentSteps, options)
  options = options or {}
  local keepExtras = options.keepExtras ~= false
  local role = string.lower(tostring(options.role or "hybrid"))
  local source = string.lower(tostring(options.source or "method"))
  local hero = Trim(options.hero)
  local order = Rebuild.GetOrder(className, spec, source, hero)

  if not order then
  return {
  ok = false,
  message = "No priority list yet for " .. tostring(className) .. " / " .. tostring(spec) .. " (" .. source .. ").",
  }
  end

  local stepSource = order.steps or {}
  local usedAoe = false
  if role == "aoe" and order.aoeSteps and #order.aoeSteps > 0 then
  stepSource = order.aoeSteps
  usedAoe = true
  end

  local built = {}
  local CG = SpellSmasherNS and SpellSmasherNS.CastGuard
  for _, t in ipairs(stepSource) do
  local name = t.name or t.label
  if name and name ~= "" then
  -- Skip pure talent passives — never put them on a cast sequence
  if CG and CG.IsPassiveSpellName and CG.IsPassiveSpellName(name) then
  -- drop
  else
  local mac = MacroForStep(t)
  if CG and CG.CombatMacroFor then
  mac = CG.CombatMacroFor(name, mac)
  end
  if mac and mac ~= "" then
  local row = {
  label = name,
  macro = mac,
  mode = t.mode or "priority",
  interval = 3,
  looped = (t.mode or "priority") ~= "once",
  }
  local lr = tonumber(t.loopRepeat)
  if lr and lr >= 2 then
  row.loopRepeat = math.min(99, math.floor(lr))
  end
  built[#built + 1] = row
  end
  end
  end
  end

  local used = {}
  for _, b in ipairs(built) do
  used[NormKey(b.label)] = true
  end

  local extras = {}
  if keepExtras and type(currentSteps) == "table" then
  for _, s in ipairs(currentSteps) do
  local k = NormKey(s.label)
  local covered = used[k] == true
  if not covered then
  for u in pairs(used) do
  if k:find(u, 1, true) or u:find(k, 1, true) then
  covered = true
  break
  end
  end
  end
  if not covered and s.label and Trim(s.label) ~= "" then
  if not (CG and CG.IsPassiveSpellName and CG.IsPassiveSpellName(s.label)) then
  local emac = s.macro or MacroForStep({ name = s.label })
  if CG and CG.CombatMacroFor then
  emac = CG.CombatMacroFor(s.label, emac)
  end
  if emac and emac ~= "" then
  local extra = {
  label = s.label,
  macro = emac,
  mode = s.mode or "priority",
  interval = s.interval or 3,
  looped = (s.mode or "priority") ~= "once",
  }
  local lr = tonumber(s.loopRepeat)
  if lr and lr >= 2 then
  extra.loopRepeat = math.min(99, math.floor(lr))
  end
  extras[#extras + 1] = extra
  used[k] = true
  end
  end
  end
  end
  end

  -- Preserve per-spell loopRepeat (xN) on template spells that still exist
  local loopByKey = {}
  if type(currentSteps) == "table" then
  for _, s in ipairs(currentSteps) do
  local lr = tonumber(s.loopRepeat)
  if lr and lr >= 2 then
  loopByKey[NormKey(s.label)] = math.min(99, math.floor(lr))
  end
  end
  end
  for _, b in ipairs(built) do
  local lr = loopByKey[NormKey(b.label)]
  if lr then
  b.loopRepeat = lr
  end
  end

  local steps = {}
  for _, b in ipairs(built) do
  steps[#steps + 1] = b
  end
  for _, e in ipairs(extras) do
  steps[#steps + 1] = e
  end

  local heroOut = hero
  if heroOut == "" and order.heroTalent then
  heroOut = tostring(order.heroTalent)
  end
  if heroOut == "" and order.talents and order.talents.hero then
  heroOut = tostring(order.talents.hero)
  end

  return {
  ok = true,
  name = order.name or (className .. " " .. spec),
  description = order.description or "",
  source = order.source or (order.engine and "SpellSmasher Engine" or "Method.gg"),
  patch = order.patch or "",
  talents = order.talents,
  heroTalent = heroOut,
  role = role,
  engine = order.engine and true or false,
  packSource = source,
  usedAoeSteps = usedAoe,
  steps = steps,
  templateCount = #built,
  extrasCount = #extras,
  }
end

local function MakeStepFromBuilt(b)
  local label = b.label or b.name or ""
  local macro = b.macro or ""
  -- Never build a step for pure talent passives (no real cast button).
  local CG = SpellSmasherNS and SpellSmasherNS.CastGuard
  if CG then
  if CG.IsPassiveSpellName and CG.IsPassiveSpellName(label) then
  return nil
  end
  if CG.CombatMacroFor then
  local fixed = CG.CombatMacroFor(label, macro)
  if not fixed then
  return nil
  end
  macro = fixed
  end
  end
  local out = {
  label = label,
  macro = macro,
  mode = b.mode or "priority",
  interval = b.interval or 3,
  looped = b.looped ~= false and (b.mode or "priority") ~= "once",
  }
  local lr = tonumber(b.loopRepeat)
  if lr and lr >= 2 then
  out.loopRepeat = math.min(99, math.floor(lr))
  end
  return out
end

local function NewId()
  return string.format("%s-%04d", tostring(time() or 0), math.random(1000, 9999))
end

--- Build one sequence object. Default role = aoe (product: one mash list from aoeSteps).
--- @param hero optional hero talent path name (Class|Spec|Hero)
function Rebuild.BuildSequenceObject(className, spec, role, characterLevel, packSource, hero)
  local lv = math.max(70, math.min(90, math.floor(tonumber(characterLevel) or 90)))
  local source = string.lower(tostring(packSource or "method"))
  local isEngine = (source == "engine" or source == "spellsmasher" or source == "ss")
  hero = Trim(hero)
  if hero == "" then
  hero = Rebuild.DefaultHero(className, spec)
  end
  -- Product default: aoeSteps only (M+ pack). "st" only if caller explicitly asks.
  local packRole = string.lower(tostring(role or "aoe"))
  if packRole ~= "aoe" and packRole ~= "st" and packRole ~= "hybrid" then
  packRole = "aoe"
  end

  local result = Rebuild.RebuildSequence(className, spec, {}, {
  keepExtras = false,
  role = packRole == "st" and "st" or "aoe",
  source = isEngine and "engine" or "method",
  hero = hero,
  })

  -- If aoe requested but no aoeSteps authored, fall back to ST steps once.
  if (not result.ok or not result.steps or #result.steps == 0) and packRole == "aoe" then
  result = Rebuild.RebuildSequence(className, spec, {}, {
  keepExtras = false,
  role = "st",
  source = isEngine and "engine" or "method",
  hero = hero,
  })
  packRole = "st"
  end

  local steps = {}
  if result.ok and result.steps then
  for _, s in ipairs(result.steps) do
  local st = MakeStepFromBuilt(s)
  if st then
  steps[#steps + 1] = st
  end
  end
  end

  local tag = isEngine and "SpellSmasher" or "Method"
  local heroLabel = hero ~= "" and hero or ""
  local name = Trim((spec or "") .. (heroLabel ~= "" and ("  -  " .. heroLabel) or "") .. " " .. tag)
  name = Trim(name:gsub("%s+", " "))

  local notes = (result.description or (isEngine and "SpellSmasher Engine starter" or "Method-style starter"))
  .. (result.patch and (" (" .. result.patch .. ")") or "")
  .. "  -  level "
  .. tostring(lv)
  .. (isEngine and "  -  SpellSmasher" or "  -  Method")
  if result.usedAoeSteps then
  notes = notes .. "  -  aoeSteps (one-button pack)."
  elseif packRole == "st" then
  notes = notes .. "  -  ST steps (no dedicated aoeSteps for this spec)."
  end

  local talentChecklist = ""
  local talentLoadout = ""
  if result.talents then
  if result.talents.recommended then
  talentChecklist = table.concat(result.talents.recommended, ", ")
  end
  if result.talents.loadout and #tostring(result.talents.loadout) >= 40 and lv >= 90 then
  talentLoadout = result.talents.loadout
  end
  end
  -- Method curated CUk when registry has one (placeholder until live snap).
  if (talentLoadout == "" or #tostring(talentLoadout) < 40) and SpellSmasherMethodLoadouts then
  local fromReg = SpellSmasherMethodLoadouts.Get(className, spec, hero)
  if fromReg and #fromReg >= 40 then
  talentLoadout = fromReg
  end
  end

  local heroFinal = hero
  if heroFinal == "" and result.heroTalent then
  heroFinal = tostring(result.heroTalent)
  end
  if heroFinal == "" and result.talents and result.talents.hero then
  heroFinal = tostring(result.talents.hero)
  end

  return {
  name = name,
  className = className,
  spec = spec,
  heroTalent = heroFinal,
  role = packRole == "hybrid" and "hybrid" or packRole,
  characterLevel = lv,
  notes = notes,
  steps = steps,
  talents = talentLoadout ~= "" and talentLoadout or talentChecklist,
  talentChecklist = talentChecklist,
  talentLoadout = talentLoadout,
  talentLoadoutSource = (talentLoadout ~= "" and "method") or "",
  packSource = isEngine and "engine" or "method",
  usedDedicatedAoe = result.usedAoeSteps and true or false,
  sequentialRepeat = 1,
  priorityRepeat = 1,
  variables = {},
  }, result
end

--- Create ONE pack from aoeSteps (default). Returns seq, packId, message
--- (legacy callers expecting 4 values: seq, nil, packId, msg)
--- @param hero optional hero talent path
function Rebuild.CreatePack(className, spec, characterLevel, packSource, hero)
  local seq, res = Rebuild.BuildSequenceObject(className, spec, "aoe", characterLevel, packSource, hero)
  if not res or not res.ok or not seq or not seq.steps or #seq.steps == 0 then
  return nil, nil, (res and res.message) or "No order for this class/spec."
  end

  local packId = NewId()
  seq.packId = packId
  seq.packMateRole = nil
  seq.role = "aoe"

  -- Method/Engine packs keep the curated Method talent CUk that matches the spell list.
  -- Do NOT overwrite with live character talents here (fresh toons / partial trees look "wrong").
  -- Live snap is only: Edit  >  From me, or Export (push YOUR tree to the site).
  local isEngine = string.lower(tostring(packSource or "")) == "engine"
  or string.lower(tostring(packSource or "")) == "spellsmasher"
  local label = isEngine and "SpellSmasher" or "Method"
  local hasMethodLo = seq.talentLoadout and #tostring(seq.talentLoadout) >= 40
  seq.talentLoadoutSource = hasMethodLo and "method" or (seq.talentLoadoutSource or "")

  local heroNote = (seq.heroTalent and seq.heroTalent ~= "") and ("  -  " .. seq.heroTalent) or ""
  local msg = string.format(
  "Created one %s pack for %s / %s%s (level %s) - %d spells from aoeSteps.\nNo separate ST list. Edit steps if you want a boss-only list.",
  label,
  className,
  spec,
  heroNote,
  tostring(characterLevel or 90),
  seq.steps and #seq.steps or 0
  )
  if seq.usedDedicatedAoe then
  msg = msg .. "\n(dedicated aoeSteps)"
  end
  if hasMethodLo then
  msg = msg
  .. "\n|cff3dd68cTalent string = Method loadout for this list|r - Copy  >  Talents  >  Import in-game.\n"
  .. "Your current tree is separate; use |cffffff00From me|r only if you want YOUR live CUk on the pack."
  else
  msg = msg .. "\n|cfff0b429No Method talent string for this spec yet.|r"
  end

  return seq, packId, msg
end

--- Rebuild current sequence from aoeSteps (product default). No pack-mate dual update.
function Rebuild.RebuildInPlace(seq, sequences, options)
  if not seq then
  return false, "No sequence."
  end
  options = options or {}
  local source = options.source or seq.packSource or "method"
  -- Always rebuild from aoeSteps unless options.role == "st" (manual override).
  local role = string.lower(tostring(options.role or "aoe"))
  if role ~= "st" then
  role = "aoe"
  end

  local hero = Trim(options.hero or seq.heroTalent)
  local result = Rebuild.RebuildSequence(seq.className, seq.spec, seq.steps or {}, {
  keepExtras = options.keepExtras == true, -- default: replace with clean aoe list
  role = role,
  source = source,
  hero = hero,
  })
  if not result.ok then
  -- Fallback to ST steps if no aoeSteps for this spec
  if role == "aoe" then
  result = Rebuild.RebuildSequence(seq.className, seq.spec, seq.steps or {}, {
  keepExtras = false,
  role = "st",
  source = source,
  hero = hero,
  })
  role = "st"
  end
  end
  if not result.ok then
  return false, result.message, false
  end

  seq.steps = {}
  for _, s in ipairs(result.steps) do
  seq.steps[#seq.steps + 1] = MakeStepFromBuilt(s)
  end
  seq.role = role == "st" and "st" or "aoe"
  seq.packSource = result.engine and "engine" or "method"
  seq.packMateRole = nil
  if result.heroTalent and result.heroTalent ~= "" then
  seq.heroTalent = result.heroTalent
  elseif hero ~= "" then
  seq.heroTalent = hero
  end
  if result.talents and result.talents.recommended then
  seq.talentChecklist = table.concat(result.talents.recommended, ", ")
  end
  -- Always re-attach Method curated CUk on Method/Engine rebuild (matches spell list).
  if result.talents and result.talents.loadout and #tostring(result.talents.loadout) >= 40 then
  seq.talentLoadout = result.talents.loadout
  seq.talents = result.talents.loadout
  seq.talentLoadoutSource = "method"
  elseif SpellSmasherMethodLoadouts then
  local fromReg = SpellSmasherMethodLoadouts.Get(seq.className, seq.spec, seq.heroTalent or hero)
  if fromReg and #fromReg >= 40 then
  seq.talentLoadout = fromReg
  seq.talents = fromReg
  seq.talentLoadoutSource = "method"
  end
  end
  seq.notes = (result.engine and "SpellSmasher rebuild: " or "Method rebuild: ")
  .. (result.name or "")
  .. (result.usedAoeSteps and "  -  aoeSteps" or "  -  steps")

  local msg = string.format(
  "Rebuilt %s (%d spells  -  %s)",
  result.engine and "SpellSmasher Engine" or "Method",
  #seq.steps,
  result.usedAoeSteps and "aoeSteps" or role
  )
  if seq.talentLoadout and #tostring(seq.talentLoadout) >= 40 then
  msg = msg .. "  -  Method talent string restored"
  end
  return true, msg, false
end

--- List-aware talent package (theme "best for my spells" practical port).
--- Uses Method recommended names + curated CUk loadout - does NOT run a full talent-tree sim.
--- @param seq table pack with className, spec, steps
--- @return ok boolean, msg string, info table|nil
function Rebuild.RecommendTalentsForList(seq)
  if not seq then
  return false, "No pack selected."
  end
  local className = seq.className or ""
  local spec = seq.spec or ""
  if className == "" or spec == "" then
  return false, "Pack needs class and spec."
  end

  local hero = Trim(seq.heroTalent)
  local order = GetMethodOrder(className, spec, hero)
  local recommended = {}
  if order and order.talents and order.talents.recommended then
  for _, n in ipairs(order.talents.recommended) do
  recommended[#recommended + 1] = tostring(n)
  end
  end

  -- Spell names on the current list
  local onList = {}
  local onListKeys = {}
  for _, st in ipairs(seq.steps or {}) do
  local lab = Trim(st.label or st.name or "")
  if lab ~= "" then
  onList[#onList + 1] = lab
  onListKeys[NormKey(lab)] = lab
  end
  end
  if #onList == 0 then
  return false, "Add spells first, then match talents to the list."
  end

  -- Intersection: Method recommended talents that appear (or relate) on the spell list
  local matched = {}
  local missingFromList = {}
  for _, rec in ipairs(recommended) do
  local k = NormKey(rec)
  if onListKeys[k] then
  matched[#matched + 1] = onListKeys[k]
  else
  -- soft match: list spell contains rec or rec contains list spell
  local soft = false
  for lk, lab in pairs(onListKeys) do
  if k ~= "" and (lk:find(k, 1, true) or k:find(lk, 1, true)) then
  matched[#matched + 1] = lab
  soft = true
  break
  end
  end
  if not soft then
  missingFromList[#missingFromList + 1] = rec
  end
  end
  end

  -- Always attach curated Method CUk when available (pairs with Method/Engine packs)
  local loadout = ""
  if order and order.talents and order.talents.loadout and #tostring(order.talents.loadout) >= 40 then
  loadout = tostring(order.talents.loadout)
  end
  if #loadout < 40 and SpellSmasherMethodLoadouts and SpellSmasherMethodLoadouts.Get then
  loadout = SpellSmasherMethodLoadouts.Get(className, spec, hero) or ""
  end

  if #loadout < 40 and #recommended == 0 then
  return false, "No Method talent package for " .. className .. " / " .. spec .. "."
  end

  if #loadout >= 40 then
  seq.talentLoadout = loadout
  seq.talents = loadout
  seq.talentLoadoutSource = "method"
  end
  if #recommended > 0 then
  seq.talentChecklist = table.concat(recommended, ", ")
  end

  local info = {
  matched = matched,
  missingFromList = missingFromList,
  recommended = recommended,
  hasLoadout = #loadout >= 40,
  variant = order and order.talents and order.talents.variant or nil,
  hero = order and order.talents and order.talents.hero or nil,
  }

  local msg = "|cff3dd68cBest talents for list|r"
  if info.variant then
  msg = msg .. "  -  " .. tostring(info.variant)
  end
  if info.hasLoadout then
  msg = msg .. "  -  Method CUk on pack"
  end
  msg = msg
  .. string.format(
  "\n|cff8b96adList spells in Method package:|r %d  -  |cff8b96adPackage extras (support talents):|r %d",
  #matched,
  #missingFromList
  )
  if #matched > 0 then
  local show = {}
  for i = 1, math.min(6, #matched) do
  show[i] = matched[i]
  end
  msg = msg .. "\n|cff3dd68cOn list:|r " .. table.concat(show, ", ") .. (#matched > 6 and "..." or "")
  end
  msg = msg .. "\n|cffffff00Copy|r  >  Ctrl+C  >  Talents  >  Import to apply the tree in game."

  return true, msg, info
end

SpellSmasherRebuild = Rebuild
return Rebuild
