--[[
  SpellSmasher compile - (c) Tracy Howard / SpellSmasher.

  OOC only:
  once steps  >  one cast each per combat engagement (skipped after fire until
  leave-combat / retarget; secure runtime enforces this)
  sequential groups  >  in order (x sequentialRepeat)
  priority groups  >  priority triangle (1 cast per click; top more often)
  reverse_priority  >  reverse triangle (1 21 321...; same slot weights, different rhythm)

  MakeStepMacrotext:
  optional Easy helpers first (only if On AND fit in 255)
  optional step-local helpers from step.macro (startattack / retarget)
  then ONE combat cast from the sequence
  NEVER multi-GCD Priority windows; NEVER add spells not on the list
]]

local NS = SpellSmasherNS
local Compile = {}
NS.Compile = Compile

local MACROTEXT_MAX = NS.MACROTEXT_MAX
local MAX_FLAT = NS.MAX_FLAT_STEPS
local Trim = NS.Trim
local NormMode = NS.NormMode

local function IsHelperMacroLine(line)
  local low = string.lower(line or "")
  return low:find("/startattack", 1, true)
  or low:find("targetenemy", 1, true)
  or low:find("/stopmacro", 1, true)
end

--- Split macro text into helper lines vs combat lines (no normalization).
function Compile.ClassifyMacroLines(text)
  local helpers, combat = {}, {}
  text = tostring(text or ""):gsub("\r\n", "\n"):gsub("\r", "\n")
  for line in (text .. "\n"):gmatch("(.-)\n") do
  line = Trim(line)
  if line ~= "" then
  if IsHelperMacroLine(line) then
  helpers[#helpers + 1] = line
  else
  combat[#combat + 1] = line
  end
  end
  end
  return helpers, combat
end

--- Pull unit/mod conditions from a cast line (for preserve-on-rename).
--- @return string|nil e.g. "@player" or "mod:shift,@cursor"
function Compile.ExtractCastConditions(line)
  line = tostring(line or "")
  local brackets = line:match("^/%w+%s*%[(.-)%]")
  if not brackets then
  return nil
  end
  local parts = {}
  for piece in (brackets .. ","):gmatch("([^,]+),") do
  piece = Trim(piece)
  local pl = piece:lower()
  if pl ~= "" and pl ~= "nochanneling" then
  parts[#parts + 1] = piece
  end
  end
  if #parts == 0 then
  return nil
  end
  return table.concat(parts, ",")
end

--- Build /cast line with nochanneling + optional unit/mod conditions.
function Compile.BuildCastLine(spellName, conditions)
  spellName = Trim(tostring(spellName or ""))
  if spellName == "" then
  return ""
  end
  -- Label typos: "Infernal Strike @player"  >  unit + clean name
  local unitFromName = spellName:match("%s+(@%w+)%s*$")
  if unitFromName then
  spellName = Trim(spellName:gsub("%s+@%w+%s*$", ""))
  if not conditions or conditions == "" then
  conditions = unitFromName
  elseif not conditions:lower():find(unitFromName:lower(), 1, true) then
  conditions = conditions .. "," .. unitFromName
  end
  end
  conditions = Trim(tostring(conditions or ""))
  local inner = "nochanneling"
  if conditions ~= "" then
  local c = conditions
  if not c:lower():find("nochanneling", 1, true) then
  inner = "nochanneling," .. c
  else
  inner = c
  if not inner:lower():find("nochanneling", 1, true) then
  inner = "nochanneling," .. inner
  end
  end
  end
  local line = "/cast [" .. inner .. "] " .. spellName
  if #line > NS.MACRO_LINE_MAX then
  line = line:sub(1, NS.MACRO_LINE_MAX)
  end
  return line
end

--- Ensure combat cast has [nochanneling] without stripping mod:/@ unit.
function Compile.NormalizeCombatCastLine(line)
  line = Trim(tostring(line or ""))
  if line == "" then
  return ""
  end
  line = line:gsub("[{}]", "")
  local low = line:lower()

  if not (low:find("^/cast") or low:find("^/use") or low:find("^/castsequence")) then
  if line:sub(1, 1) ~= "/" then
  return Compile.BuildCastLine(line, nil)
  end
  end

  if low:find("^/castsequence") then
  if not low:find("nochanneling", 1, true) then
  local prefix, brackets, rest = line:match("^(/[Cc]astsequence)%s*%[(.-)%]%s*(.+)$")
  if prefix and brackets and rest then
  return prefix .. " [nochanneling," .. brackets .. "] " .. rest
  end
  end
  return line
  end

  if not low:find("^/cast") and not low:find("^/use") then
  return line
  end

  local cmd = low:find("^/use") and "use" or "cast"
  local prefix = line:match("^(/[Cc]ast)") or line:match("^(/[Uu]se)") or ("/" .. cmd)

  local brackets, spell = line:match("^/%w+%s*%[(.-)%]%s*(.+)$")
  if brackets and spell then
  spell = Trim(spell)
  local trail = spell:match("%s+(@%w+)%s*$")
  if trail then
  spell = Trim(spell:gsub("%s+@%w+%s*$", ""))
  if not brackets:lower():find(trail:lower(), 1, true) then
  brackets = brackets .. "," .. trail
  end
  end
  if not brackets:lower():find("nochanneling", 1, true) then
  brackets = "nochanneling," .. brackets
  end
  brackets = brackets:gsub("%s*,%s*", ","):gsub(",+", ","):gsub("^,", ""):gsub(",$", "")
  local out = prefix .. " [" .. brackets .. "] " .. spell
  if #out > NS.MACRO_LINE_MAX then
  out = out:sub(1, NS.MACRO_LINE_MAX)
  end
  return out
  end

  local rest = line:match("^/%w+%s+(.+)$")
  if rest and not rest:match("^%[") then
  rest = Trim(rest)
  local trail = rest:match("%s+(@%w+)%s*$")
  local spell = rest
  local cond = nil
  if trail then
  spell = Trim(rest:gsub("%s+@%w+%s*$", ""))
  cond = trail
  end
  return Compile.BuildCastLine(spell, cond)
  end

  if #line > NS.MACRO_LINE_MAX then
  line = line:sub(1, NS.MACRO_LINE_MAX)
  end
  return line
end

function Compile.SpellNameFromCastLine(line)
  line = tostring(line or "")
  -- Prefer last combat line if multi-line (helpers + cast)
  local lastCombat = nil
  for sub in (line .. "\n"):gmatch("(.-)\n") do
  sub = Trim(sub)
  if sub ~= "" and not IsHelperMacroLine(sub) then
  lastCombat = sub
  end
  end
  if lastCombat then
  line = lastCombat
  end
  local name = line:match("%]%s*(.+)$") or line:match("^/cast%s+(.+)$") or line
  name = Trim(name:gsub("\n.*", ""))
  return name
end

--- Step  >  macro body for compile: step helpers (if any) + ONE combat cast.
--- Extra combat lines after the first are dropped (no multi-GCD window).
function Compile.StepToCastLine(step)
  if type(step) ~= "table" then
  return nil
  end
  local helpers, combat = Compile.ClassifyMacroLines(step.macro or "")
  local castLine = nil
  if #combat > 0 then
  castLine = Compile.NormalizeCombatCastLine(combat[1])
  else
  local label = Trim(step.label)
  if label == "" then
  return nil
  end
  label = label:gsub("[\r\n;]", " ")
  if label:find("+", 1, true) then
  label = Trim(label:match("^([^+]+)") or label)
  end
  castLine = Compile.NormalizeCombatCastLine("/cast " .. label)
  end
  if not castLine or castLine == "" then
  return nil
  end
  if #helpers == 0 then
  return castLine
  end
  local parts = {}
  for _, h in ipairs(helpers) do
  parts[#parts + 1] = h
  end
  parts[#parts + 1] = castLine
  return table.concat(parts, "\n")
end

--- Contiguous mode groups (same as site / prior SSM).
function Compile.GetGroups(seq)
  local steps = seq and seq.steps
  if type(steps) ~= "table" or #steps == 0 then
  return {}
  end
  local groups = {}
  local i = 1
  while i <= #steps do
  local mode = NormMode(steps[i].mode)
  local startIndex = i
  while i <= #steps and NormMode(steps[i].mode) == mode do
  i = i + 1
  end
  local rep = 1
  if mode == "sequential" then
  rep = math.max(1, math.min(99, math.floor(tonumber(seq.sequentialRepeat) or 1)))
  elseif mode == "priority" or mode == "reverse_priority" then
  rep = math.max(1, math.min(99, math.floor(tonumber(seq.priorityRepeat) or 1)))
  end
  groups[#groups + 1] = {
  mode = mode,
  startIndex = startIndex,
  endIndex = i - 1,
  count = i - startIndex,
  rep = rep,
  }
  end
  return groups
end

--- Priority triangle expand:
--- [A,B,C,D]  >  A | A B | A B C | A B C D  as separate clicks (top more often)
function Compile.ExpandPriorityTriangle(actionList)
  local n = #actionList
  if n == 0 then
  return {}
  end
  if n == 1 then
  return { actionList[1] }
  end
  local looplimit = 0
  for x = 1, n do
  looplimit = looplimit + x
  end
  local out = {}
  local limit = 1
  local step = 1
  for _ = 1, looplimit do
  out[#out + 1] = actionList[step]
  if step == limit then
  limit = limit % n + 1
  step = 1
  else
  step = step + 1
  end
  end
  return out
end

--- Reverse Priority expand ("1 21 321 4321"):
--- [A,B,C]  >  A | B A | C B A  as separate clicks (same slot weights as Priority)
function Compile.ExpandReversePriorityTriangle(actionList)
  local n = #actionList
  if n == 0 then
  return {}
  end
  if n == 1 then
  return { actionList[1] }
  end
  local out = {}
  for i = 1, n do
  for j = i, 1, -1 do
  out[#out + 1] = actionList[j]
  end
  end
  return out
end

--- Slot weight for position i (1-based) in an n-slot Priority triangle: appears (n-i+1) times.
function Compile.PrioritySlotWeight(n, i)
  n = tonumber(n) or 0
  i = tonumber(i) or 0
  if n < 1 or i < 1 or i > n then
  return 0
  end
  return n - i + 1
end

function Compile.PriorityTriangleTotal(n)
  n = tonumber(n) or 0
  if n < 1 then
  return 0
  end
  return math.floor(n * (n + 1) / 2)
end

--- OOC preview: stepIndex  >  share of Priority clicks (0-100) for contiguous Priority blocks.
--- Uses the same loopRepeat expansion as CompileFlatSteps.
function Compile.ComputeStepWeightPct(seq)
  local out = {}
  if type(seq) ~= "table" or type(seq.steps) ~= "table" then
  return out
  end
  local steps = seq.steps
  local groups = Compile.GetGroups(seq)
  for _, g in ipairs(groups) do
  if g.mode == "priority" or g.mode == "reverse_priority" then
  local slots = {} -- { stepIndex, label }
  for si = g.startIndex, g.endIndex do
  local st = steps[si]
  local stepRep = math.max(1, math.min(99, math.floor(tonumber(st and st.loopRepeat) or 1)))
  local lab = Trim(st and st.label or "")
  for _ = 1, stepRep do
  slots[#slots + 1] = { index = si, label = lab }
  end
  end
  local n = #slots
  local total = Compile.PriorityTriangleTotal(n)
  if total > 0 then
  local sumByIndex = {}
  for i, slot in ipairs(slots) do
  local w = Compile.PrioritySlotWeight(n, i)
  sumByIndex[slot.index] = (sumByIndex[slot.index] or 0) + w
  end
  for idx, w in pairs(sumByIndex) do
  out[idx] = math.floor((w / total) * 100 + 0.5)
  end
  end
  end
  end
  return out
end

function Compile.SequenceHasSpellLabel(seq, spellName)
  if type(seq) ~= "table" or type(seq.steps) ~= "table" then
  return false
  end
  local want = Trim(spellName):lower()
  if want == "" then
  return false
  end
  for _, st in ipairs(seq.steps) do
  local lab = Trim(st and st.label or ""):lower()
  if lab == want or lab:find(want, 1, true) then
  return true
  end
  local mac = tostring(st and st.macro or ""):lower()
  if mac:find(want, 1, true) then
  return true
  end
  end
  return false
end

function Compile.DefensiveSpellName(seq)
  local className = (seq and Trim(seq.className) ~= "" and Trim(seq.className)) or NS.ActiveClassName()
  local spell = className and NS.DEFENSIVE_BY_CLASS[className]
  if not spell then
  return nil
  end
  if className == "Demon Hunter" then
  spell = "Demon Spikes"
  elseif className == "Warrior" then
  local spec = seq and Trim(seq.spec or "") or ""
  if spec == "Protection" then
  spell = "Shield Block"
  elseif spec == "Fury" or spec == "Arms" then
  spell = "Die by the Sword"
  end
  end
  if Compile.SequenceHasSpellLabel(seq, spell) then
  return nil -- already on list  >  do not also prepend
  end
  return spell
end

--- Easy helpers only (user toggles). No secret pack KeyPress auto-inject.
function Compile.HelperPrefixLines(seq)
  local db = NS.db
  local out = {}
  if not db then
  return out
  end
  if db.helperAutoAttack then
  out[#out + 1] = "/startattack"
  end
  if db.helperRetarget then
  out[#out + 1] = "/targetenemy [noharm][dead]"
  end
  if db.helperDefensive then
  local def = Compile.DefensiveSpellName(seq)
  if def then
  out[#out + 1] = "/cast " .. def
  end
  end
  return out
end

--- ONE combat cast + optional helpers (Easy + step-local) if they fit in 255.
--- Combat cast is always reserved first; helpers fill remaining room.
function Compile.MakeStepMacrotext(castLine, seq)
  castLine = tostring(castLine or ""):gsub("[{}]", "")
  local stepHelpers, combatLines = Compile.ClassifyMacroLines(castLine)
  local castBody = ""
  if #combatLines > 0 then
  castBody = Compile.NormalizeCombatCastLine(combatLines[1])
  elseif Trim(castLine) ~= "" and not IsHelperMacroLine(castLine) then
  castBody = Compile.NormalizeCombatCastLine(Trim(castLine))
  end

  local parts = {}
  local used = 0
  local function tryAdd(line)
  line = Trim(tostring(line or ""))
  if line == "" then
  return true
  end
  local add = (used > 0 and 1 or 0) + #line
  if used + add > MACROTEXT_MAX then
  return false
  end
  parts[#parts + 1] = line
  used = used + add
  return true
  end

  local castLen = #castBody
  local roomForHelpers = MACROTEXT_MAX - castLen
  if castLen > 0 and roomForHelpers > 0 then
  roomForHelpers = roomForHelpers - 1
  end

  local helperUsed = 0
  local function tryHelper(h)
  h = Trim(tostring(h or ""))
  if h == "" then
  return
  end
  local add = (helperUsed > 0 and 1 or 0) + #h
  if helperUsed + add > roomForHelpers then
  return
  end
  if tryAdd(h) then
  helperUsed = helperUsed + add
  end
  end

  -- Easy toggles first, then any helpers authored on the step
  for _, h in ipairs(Compile.HelperPrefixLines(seq)) do
  tryHelper(h)
  end
  for _, h in ipairs(stepHelpers) do
  tryHelper(h)
  end
  if castBody ~= "" then
  tryAdd(castBody)
  end

  if #parts == 0 then
  return "/startattack"
  end
  return table.concat(parts, "\n")
end

--- OOC compile: ONLY spells from seq.steps (+ Easy helpers if On).
function Compile.CompileFlatSteps(seq)
  local empty = { { macrotext = Compile.MakeStepMacrotext("", seq), once = false } }
  if type(seq) ~= "table" or type(seq.steps) ~= "table" or #seq.steps == 0 then
  return empty, {
  once = 0,
  seq = 0,
  pri = 0,
  total = 0,
  priorityMode = "priority-triangle",
  stepWeightPct = {},
  }
  end

  -- Drop passive / non-cast steps so mash never wastes presses on talent nodes
  local steps = seq.steps
  if NS.CastGuard and NS.CastGuard.FilterSteps then
  local filtered, dropped = NS.CastGuard.FilterSteps(steps)
  if type(filtered) == "table" then
  steps = filtered
  if dropped and #dropped > 0 then
  seq.steps = filtered
  end
  end
  end
  local tmpSeq = seq
  if steps ~= seq.steps then
  tmpSeq = {}
  for k, v in pairs(seq) do
  tmpSeq[k] = v
  end
  tmpSeq.steps = steps
  end
  local groups = Compile.GetGroups(tmpSeq)
  local castOrder = {}
  local hasReverse = false
  local hasForward = false
  local stats = {
  once = 0,
  seq = 0,
  pri = 0,
  total = 0,
  labels = {},
  order = {},
  priorityMode = "priority-triangle",
  groupSummary = {},
  truncated = false,
  stepWeightPct = Compile.ComputeStepWeightPct(tmpSeq),
  }

  --- Each flat step is { macrotext = string, once = bool } for secure once-per-combat.
  local function pushLine(line, bucket, label)
  if not line then
  return
  end
  if #castOrder >= MAX_FLAT then
  stats.truncated = true
  return
  end
  castOrder[#castOrder + 1] = {
  macrotext = line,
  once = (bucket == "once"),
  }
  if bucket then
  stats[bucket] = (stats[bucket] or 0) + 1
  end
  local key = Trim(label or "")
  if key == "" then
  key = Compile.SpellNameFromCastLine(line) or line
  key = Trim(tostring(key):gsub("\n.*", ""))
  end
  if key ~= "" then
  stats.labels[key] = (stats.labels[key] or 0) + 1
  end
  stats.order[#stats.order + 1] = key ~= "" and key or line
  end

  for _, g in ipairs(groups) do
  stats.groupSummary[#stats.groupSummary + 1] = {
  mode = g.mode,
  count = g.count,
  rep = g.rep,
  from = g.startIndex,
  to = g.endIndex,
  }
  local children = {}
  local childLabels = {}
  for si = g.startIndex, g.endIndex do
  local line = Compile.StepToCastLine(steps[si])
  local lab = Trim(steps[si] and steps[si].label or "")
  local stepRep = math.max(1, math.min(99, math.floor(tonumber(steps[si] and steps[si].loopRepeat) or 1)))
  if line and line ~= "" then
  local times = (g.mode == "once") and 1 or stepRep
  for _ = 1, times do
  children[#children + 1] = line
  childLabels[#childLabels + 1] = lab ~= "" and lab or line
  end
  end
  end

  local rep = math.max(1, math.min(99, math.floor(tonumber(g.rep) or 1)))
  if #children == 0 then
  -- skip
  elseif g.mode == "once" then
  for ci, line in ipairs(children) do
  pushLine(line, "once", childLabels[ci])
  end
  elseif g.mode == "sequential" then
  for _ = 1, rep do
  for ci, line in ipairs(children) do
  pushLine(line, "seq", childLabels[ci])
  end
  end
  elseif g.mode == "reverse_priority" then
  hasReverse = true
  for _ = 1, rep do
  local expanded = Compile.ExpandReversePriorityTriangle(children)
  for _, line in ipairs(expanded) do
  local lab = Compile.SpellNameFromCastLine(line) or line
  pushLine(line, "pri", lab)
  end
  end
  else
  -- priority (default)
  hasForward = true
  for _ = 1, rep do
  local expanded = Compile.ExpandPriorityTriangle(children)
  for _, line in ipairs(expanded) do
  local lab = Compile.SpellNameFromCastLine(line) or line
  pushLine(line, "pri", lab)
  end
  end
  end
  end

  if hasReverse and not hasForward then
  stats.priorityMode = "reverse-triangle"
  elseif hasReverse and hasForward then
  stats.priorityMode = "priority-triangle+reverse"
  else
  stats.priorityMode = "priority-triangle"
  end

  stats.total = #castOrder
  if stats.truncated then
  NS.Print("|cffff6b7aCompile capped at " .. MAX_FLAT .. " clicks|r - lower Priority/Seq Repeat.")
  end
  if #castOrder == 0 then
  return empty, stats
  end

  local macros = {}
  for i = 1, #castOrder do
  local entry = castOrder[i]
  local raw = type(entry) == "table" and entry.macrotext or entry
  macros[i] = {
  macrotext = Compile.MakeStepMacrotext(raw, seq),
  once = type(entry) == "table" and entry.once and true or false,
  }
  end
  return macros, stats
end

--- Normalize a CompileFlatSteps entry to macrotext string.
function Compile.FlatStepMacrotext(entry)
  if type(entry) == "table" then
  return tostring(entry.macrotext or entry[1] or "")
  end
  return tostring(entry or "")
end

function Compile.FlatStepIsOnce(entry)
  return type(entry) == "table" and entry.once == true
end
