--[[
  SpellSmasher spotlight tour (theme-parity coach).

  Design research (applied to avoid common addon bugs):
  - Dim cutout via 4 rectangles (top/bottom/left/right), not SetClipsChildren hole
    (clipping + scale issues across UI scale; 4-panel is used by many coach UIs).
  - Custom green glow border on target - NOT ActionButton_ShowOverlayGlow
    (that API is for action-button templates; fails/no-ops on plain Frames).
  - Positions from frame:GetRect() / GetLeft-GetBottom (UIParent space).
  - Tour root on UIParent, FULLSCREEN_DIALOG, high frame level.
  - Dim EnableMouse(true) so fat-finger clicks do not hit underlying UI;
    card + buttons sit above dim and receive clicks.
  - No protected clipboard / secure taint in tour path.
  - Combat: tour still shows (read-only); sample pack create deferred if locked.
  - Versioned "done" key (like theme tour-v20) so rewrites re-flash Help CTA.
  - ASCII-only strings (no Unicode tofu on default WoW fonts).
  - C_Timer.After for layout after tab show (frames need a frame to GetRect).

  Public:
    NS.Tour.Register(id, frame)
    NS.Tour.Bind(hooks)
    NS.Tour.Start(page)
    NS.Tour.Stop(markDone, statusMsg)
    NS.Tour.IsActive()
    NS.Tour.MaybeAutoShow()
    NS.Tour.SyncHelpCta(helpBtn)
    NS.Tour.IsDone()
]]

local NS = SpellSmasherNS
if not NS then
  return
end

local Tour = {}
NS.Tour = Tour

-- Bump when step content or targets change (re-flash Help CTA like theme v21).
Tour.VERSION = 5
Tour.VERSION_LABEL = "v5"

local C = NS.C
local targets = {} -- id -> frame
local hooks = {} -- showTab, getUI, getMain, setStatus, ensureSample, refresh
local active = false
local pageIndex = 1
local root, dim, glow, card, titleFS, bodyFS, progFS, badgeFS, progFill
local dimParts = {} -- t,b,l,r
local alsoGlows = {} -- secondary glow frames
local layoutTicker
local helpCtaBtn

---------------------------------------------------------------------------
-- Steps (addon-shaped theme path: full happy path, not site Advanced tree)
---------------------------------------------------------------------------
--[[
  Fields:
    id, title, body, bullets (table of strings), note,
    tab ("list"|"new"|"import"|"editor"|nil),
    target (registry id or nil for center),
    also (table of registry ids),
    place ("side"|"center"|"bottom"),
    ensureSample (bool) - create/select a Method pack if needed
    cta (Next button label override)
]]
local STEPS = {
  {
    id = "welcome",
    title = "Step 1 - What SpellSmasher is",
    body = "Green glow = look here. Next walks one step at a time. Esc or Skip closes.\n\n"
      .. "You mash |cffffff00one key|r. The addon only casts spells |cffffff00on your Active pack|r.\n"
      .. "Not Blizzard's adaptive Single-Button Assist.\n\n"
      .. "|cff6c8cffToday's path|r\n"
      .. "1) Set me up (ST + AoE Method packs + Match talents)\n"
      .. "2) Bind 1-0 (key only - do not put a mash macro on a bar)\n"
      .. "3) Mash. Edit anytime.\n\n"
      .. "Same steps live on spellsmasher.com (Tutorial).",
    bullets = {
      "Green hole + glow = this control",
      "Next / Back - Esc or Skip closes",
      "Replay anytime from Tutorial",
    },
    tab = "list",
    target = "help",
    also = { "tabList" },
    place = "center",
    cta = "Start tour",
  },
  {
    id = "setmeup",
    title = "Step 2 - Set me up (do this first)",
    body = "Look at the green |cff3dd68cSet me up|r button.\n\n"
      .. "|cff6c8cffWhat it does (one click)|r\n"
      .. "1) Reads this character (class / spec / hero / level).\n"
      .. "2) Creates |cffffff00two|r Method packs: ST and AoE (two list rows).\n"
      .. "3) Makes one |cffffff00* ACTIVE|r (the ST/AoE pill you have selected; default AoE).\n"
      .. "4) Opens Edit and runs |cff3dd68cMatch talents|r (out of combat).\n\n"
      .. "Do |cffffff00not|r hold Shift for ST vs AoE. Two pills, two packs.\n"
      .. "Shared Method gold may ask confirm - check hero nodes in N after.",
    bullets = {
      "Two packs - not Shift, not two lists on one pack",
      "Match spends the Method package on your tree",
      "Leave combat first if Match is grey",
    },
    tab = "list",
    target = "setMeUp",
    also = { "newFromList", "listHead" },
    place = "side",
  },
  {
    id = "list",
    title = "Step 3 - List: Active vs the other pack",
    body = "You should see two rows: one ST, one AoE.\n\n"
      .. "|cff6c8cffWhat to do|r\n"
      .. "1) |cff3dd68cUse|r on the pack you want to mash (* ACTIVE).\n"
      .. "2) Bosses = ST pack. Packs of mobs = AoE pack. Swap Use between pulls.\n"
      .. "3) |cffffff00This class|r hides other classes.\n"
      .. "4) Export selected / Export all for backup or the website.\n"
      .. "5) |cff3dd68cNew pack|r if you want one list only (Method or Engine advanced).",
    bullets = {
      "Active = the mash key. The other pack sits until you Use it",
      "Import appends - never wipes",
      "Delete extras anytime",
    },
    tab = "list",
    target = "listUse",
    also = { "listEdit", "listFilter", "exportSelected" },
    place = "side",
    ensureSample = true,
  },
  {
    id = "pills",
    title = "Step 4 - ST and AoE pills",
    body = "Green glow is the LIST pills on New pack.\n\n"
      .. "|cff6c8cffWhat to do|r\n"
      .. "1) Pick class, spec, hero.\n"
      .. "2) Tap |cff3dd68cAoE / packs|r or |cffffff00Single target|r.\n"
      .. "3) |cff3dd68cCreate Method pack|r makes |cffffff00one|r list for that pill.\n"
      .. "4) Engine (advanced) is the other starter.\n\n"
      .. "Edit also has AoE / ST pills. They |cffffff00rebuild|r that pack's list (overwrites custom steps).",
    bullets = {
      "AoE = Method aoeSteps. ST = Method steps",
      "15 specs share one list (Vengeance is one) - same spells is correct",
      "Set me up already made both packs for you",
    },
    tab = "new",
    target = "roleHost",
    also = { "methodCreate", "engineCreate", "classHost", "heroHost" },
    place = "side",
  },
  {
    id = "import",
    title = "Step 5 - Import from the website",
    body = "Other start: phone or PC at spellsmasher.com.\n\n"
      .. "|cff6c8cffWhat to do|r\n"
      .. "1) Site: pick ST or AoE > Method pack > Export SpellSmasher pack > Copy.\n"
      .. "2) Paste the whole string here (must start with |cff3dd68c!SpellSmasher!|r).\n"
      .. "3) Click |cff3dd68cImport|r. Packs are |cffffff00added|r. First new pack becomes Active.\n"
      .. "4) Bind + Match talents if the site did not already attach a package.",
    bullets = {
      "Do not cut the start or end of the string",
      "Import never wipes packs you already have",
      "Site Help has a full Vengeance sample",
    },
    tab = "import",
    target = "importBtn",
    also = { "importBox", "tabImport" },
    place = "side",
  },
  {
    id = "edit",
    title = "Step 6 - Edit: name, pills, Save",
    body = "You are on |cff3dd68cEdit|r. One pack at a time.\n\n"
      .. "|cff6c8cffWhat to do|r\n"
      .. "1) Rename at the top + Enter.\n"
      .. "2) Footer |cff3dd68cAoE|r / |cffffff00ST|r pills rebuild this pack's Method list.\n"
      .. "3) Spell rows = what mash will press.\n"
      .. "4) |cff3dd68cSave|r writes and recompiles.\n\n"
      .. "Talent code is hidden on purpose. You will see |cffffff00package ready (N chars)|r.",
    bullets = {
      "Save after edits",
      "Rebuild overwrites custom steps",
      "DPS ground spells use @cursor (Meteor, Blizzard). Tanks stay @player",
    },
    tab = "editor",
    target = "packName",
    also = { "editRoleAoe", "editRoleSt", "saveBtn", "rebuildBtn" },
    place = "side",
    ensureSample = true,
  },
  {
    id = "talents",
    title = "Step 7 - Match talents",
    body = "Green glow is |cff3dd68cMatch talents|r.\n\n"
      .. "|cff6c8cffWhat to do|r\n"
      .. "1) Leave combat. Class and spec must match this pack.\n"
      .. "2) Press |cff3dd68cMatch talents|r. It attaches the Method package, spends it, then verifies.\n"
      .. "3) Shared gold (proxy) asks confirm first - then check hero nodes in N.\n"
      .. "4) If Match cannot finish: |cffffff00Copy|r > Ctrl+C > N > Import.\n\n"
      .. "Set me up already ran Match once. Run it again after a level-up.",
    bullets = {
      "Mash never changes talents by itself",
      "Copy is the fallback - not a second tree brain",
      "One tree: Match the Active pack",
    },
    tab = "editor",
    target = "talentMatch",
    also = { "talentCopy", "talentZone" },
    place = "side",
    ensureSample = true,
  },
  {
    id = "modes",
    title = "Step 8 - Spell modes (how mash walks the list)",
    body = "Click the mode pill on a row to cycle.\n\n"
      .. "|cffffff00Once|r - opener. Used on the press that arms it, then skipped until leave combat.\n"
      .. "|cffffff00In order|r - recipe top to bottom.\n"
      .. "|cffffff00Priority|r - main spam. Top of the block more often. % = share.\n"
      .. "|cffffff00Rev Pri|r - reverse triangle (rare).\n\n"
      .. "Mash |cffffff00always advances|r. It cannot wait for a cast to land (secure rule).\n"
      .. "Typical: Once / short In order, then a long Priority block.",
    bullets = {
      "Priority top = tried more often",
      "Once marks on press, not on a successful hit",
      "Reorder with the arrows",
    },
    tab = "editor",
    target = "stepMode",
    also = { "stepList", "addSpell" },
    place = "side",
    ensureSample = true,
  },
  {
    id = "bind",
    title = "Step 9 - Bind the mash key",
    body = "Green glow is the mash key bar.\n\n"
      .. "|cff6c8cffWhat to do|r\n"
      .. "1) Click |cffffff001-0|r to bind. Or Esc > Options > Key Bindings > SpellSmasher.\n"
      .. "2) |cfff0b429Do not|r drag a SpellSmasher macro onto an action bar (Midnight taint).\n"
      .. "3) Mash. Only |cffffff00* ACTIVE|r plays. Swap Use to the other pack for ST vs AoE.\n\n"
      .. "Helpers on the very bottom (AA / RT / DEF) stay Off unless you want them.",
    bullets = {
      "Keybind only",
      "Active pack = what you mash",
      "Helpers never invent extra combat spells",
    },
    tab = "list",
    target = "keyBar",
    also = { "helperAA", "helperRT", "helperDEF" },
    place = "bottom",
  },
  {
    id = "done",
    title = "Step 10 - Done - go play",
    body = "You walked the live path:\n"
      .. "Set me up > two packs > Use > Match talents > bind 1-0 > mash.\n\n"
      .. "Website: spellsmasher.com Tutorial (same steps, ST/AoE pills, Export).\n"
      .. "Written sample: spellsmasher.com/help/\n\n"
      .. "Replay: |cffffff00Tutorial|r or |cffffff00/ssm help|r.\n"
      .. "Missing a pack? |cffffff00Lost sequence?|r next to Tutorial (or |cffffff00/ssm lost|r).",
    bullets = {
      "Set me up = ST + AoE + Match",
      "Pills, not Shift",
      "You still press the key",
    },
    tab = "list",
    target = "help",
    also = { "setMeUp", "tabList" },
    place = "center",
    cta = "Finish",
  },
}

-- Classic-like: Set me up (ST + AoE, known-only). No Match / Method gold (no CUk).
local function ClassicFlavorSteps(label, extra)
  extra = extra or ""
  return {
    {
      id = "welcome",
      title = "Step 1 - " .. label,
      body = "Green glow = look here. Next walks one step at a time. Esc or Skip closes.\n\n"
        .. "You mash |cffffff00one key|r. The addon only casts spells |cffffff00on your Active pack|r.\n\n"
        .. "|cff6c8cff" .. label .. " path|r\n"
        .. "1) |cff3dd68cSet me up|r (ST + AoE for this class; known spells only)\n"
        .. "2) Bind 1-0 (key only)\n"
        .. "3) Mash.\n\n"
        .. extra
        .. "No Match talents on " .. label .. " (no loadout strings).",
      bullets = {
        "Same mash engine as Midnight",
        "Set me up builds the " .. label .. " lists",
        "Match / Method gold stay on Midnight",
      },
      tab = "list",
      target = "help",
      also = { "tabList" },
      place = "center",
      cta = "Start tour",
    },
    {
      id = "setmeup",
      title = "Step 2 - Set me up (do this first)",
      body = "Look at the green |cff3dd68cSet me up|r button.\n\n"
        .. "|cff6c8cffWhat it does (one click)|r\n"
        .. "1) Reads this character (class / spec).\n"
        .. "2) Creates |cffffff00two|r packs: ST and AoE (two list rows).\n"
        .. "3) Keeps only spells |cffffff00in your spellbook|r (dings and trainer learns refresh).\n"
        .. "4) Makes one |cffffff00* ACTIVE|r (the ST/AoE pill you have selected; default AoE).\n\n"
        .. "Do |cffffff00not|r hold Shift for ST vs AoE. Two pills, two packs.\n"
        .. "No Match talents on " .. label .. ". Import still adds, never wipes.",
      bullets = {
        "Two packs - not Shift",
        "Known spells only",
        "No talent Apply on " .. label,
      },
      tab = "list",
      target = "setMeUp",
      also = { "newFromList", "listHead" },
      place = "side",
    },
    {
      id = "bind",
      title = "Step 3 - Bind the mash key",
      body = "Click |cffffff001-0|r to bind. Or Esc > Options > Key Bindings > SpellSmasher.\n\n"
        .. "Mash. Only |cffffff00* ACTIVE|r plays. Swap Use to the other pack for ST vs AoE.",
      bullets = {
        "Keybind only",
        "Active pack = what you mash",
      },
      tab = "list",
      target = "keyBar",
      place = "bottom",
    },
    {
      id = "done",
      title = "Step 4 - Done - go play",
      body = label .. ": |cff3dd68cSet me up|r > bind > mash.\n\n"
        .. extra
        .. "Import still adds (never wipes).\n"
        .. "Replay: |cffffff00Tutorial|r or |cffffff00/ssm help|r.\n"
        .. "Website: pick " .. label .. " on the first screen, then Tutorial.",
      bullets = {
        "Same iron rules: list-only casts",
        "You still press the key",
      },
      tab = "list",
      target = "help",
      place = "center",
      cta = "Finish",
    },
  }
end

local ERA_STEPS = ClassicFlavorSteps(
  "Classic Era",
  "Vanilla Classic. 9 classes, 27 trees, level 1-60. DH / Evoker / DK / Monk will not Import.\n\n"
)
local TBC_STEPS = ClassicFlavorSteps(
  "TBC Anniversary",
  "Burning Crusade. Same 9 classes as Era, 27 trees, level 1-70. TBC buttons (Steady Shot, Crusader Strike).\n\n"
)
local MOP_STEPS = ClassicFlavorSteps(
  "MoP Classic",
  "Mists. 11 classes, 34 trees, level 1-90. Monk, Death Knight, and Guardian are here. Rogue is Combat.\n\n"
)

local function ActiveSteps()
  if NS.IsMistsFlavor then
    return MOP_STEPS
  end
  if NS.IsTbcFlavor then
    return TBC_STEPS
  end
  if NS.IsClassicFlavor then
    return ERA_STEPS
  end
  return STEPS
end

---------------------------------------------------------------------------
-- Utils
---------------------------------------------------------------------------
local function SetFS(fs, color)
  if not fs then
    return
  end
  color = color or C.text
  fs:SetTextColor(color[1], color[2], color[3], color[4] or 1)
end

local function SafeCall(fn, ...)
  if type(fn) ~= "function" then
    return
  end
  local ok, err = pcall(fn, ...)
  if not ok and NS.Print then
    -- silent in production; only print rare tour errors once
  end
  return ok, err
end

local function GetRectSafe(frame)
  if not frame or not frame.GetRect then
    return nil
  end
  if frame.IsForbidden and frame:IsForbidden() then
    return nil
  end
  if not frame:IsShown() and not (frame.IsVisible and frame:IsVisible()) then
    -- allow GetRect on shown-but-parent-hidden carefully
  end
  if not frame:IsVisible() then
    return nil
  end
  local l, b, w, h = frame:GetRect()
  if not l or not w or w <= 0 or not h or h <= 0 then
    return nil
  end
  return l, b, w, h
end

local function PadRect(l, b, w, h, pad)
  pad = pad or 6
  return l - pad, b - pad, w + pad * 2, h + pad * 2
end

---------------------------------------------------------------------------
-- Registry
---------------------------------------------------------------------------
function Tour.Register(id, frame)
  if type(id) ~= "string" or id == "" then
    return
  end
  if frame then
    targets[id] = frame
  end
end

function Tour.RegisterMany(map)
  if type(map) ~= "table" then
    return
  end
  for id, fr in pairs(map) do
    Tour.Register(id, fr)
  end
end

function Tour.GetTarget(id)
  return targets[id]
end

function Tour.Bind(h)
  hooks = h or {}
end

function Tour.IsActive()
  return active
end

function Tour.IsDone()
  local db = NS.db
  if not db then
    return false
  end
  local ver = tonumber(db.coachTourVersion) or 0
  if ver < Tour.VERSION then
    return false
  end
  return db.coachSeen and true or false
end

function Tour.MarkDone()
  local db = NS.db
  if not db then
    return
  end
  db.coachSeen = true
  db.coachTourVersion = Tour.VERSION
end

---------------------------------------------------------------------------
-- Sample pack (ensure Edit is not empty)
---------------------------------------------------------------------------
local function EnsureSamplePack()
  if type(hooks.ensureSample) == "function" then
    return hooks.ensureSample()
  end
  return false
end

---------------------------------------------------------------------------
-- DOM: dim + glow + card
---------------------------------------------------------------------------
local function EnsureRoot()
  if root then
    return
  end

  root = CreateFrame("Frame", "SpellSmasherTourRoot", UIParent)
  root:SetAllPoints(UIParent)
  root:SetFrameStrata("FULLSCREEN_DIALOG")
  root:SetFrameLevel(500)
  root:EnableMouse(false)
  root:Hide()

  -- Dim container (mouse blocks accidental UI clicks under tour)
  dim = CreateFrame("Frame", nil, root)
  dim:SetAllPoints(root)
  dim:EnableMouse(true)
  dim:SetFrameLevel(1)
  -- Swallow clicks (do not close on dim tap - same as theme)
  dim:SetScript("OnMouseDown", function() end)

  local function MakeDimPart()
    local f = CreateFrame("Frame", nil, dim, "BackdropTemplate")
    f:SetFrameLevel(2)
    f.bg = f:CreateTexture(nil, "BACKGROUND")
    f.bg:SetAllPoints()
    f.bg:SetColorTexture(0.02, 0.03, 0.05, 0.72)
    f:EnableMouse(true)
    f:Hide()
    return f
  end
  dimParts.t = MakeDimPart()
  dimParts.b = MakeDimPart()
  dimParts.l = MakeDimPart()
  dimParts.r = MakeDimPart()
  dimParts.full = MakeDimPart()

  -- Primary glow ring (parent root, above dim)
  glow = CreateFrame("Frame", nil, root, "BackdropTemplate")
  glow:SetFrameLevel(10)
  glow:SetBackdrop({
    edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
    edgeSize = 16,
    insets = { left = 3, right = 3, top = 3, bottom = 3 },
  })
  glow:SetBackdropBorderColor(C.green[1], C.green[2], C.green[3], 1)
  glow:SetBackdropColor(0, 0, 0, 0)
  -- Soft fill
  glow.fill = glow:CreateTexture(nil, "BACKGROUND")
  glow.fill:SetAllPoints()
  glow.fill:SetColorTexture(C.green[1], C.green[2], C.green[3], 0.12)
  glow:EnableMouse(false)
  glow:Hide()

  -- Secondary glows (also targets)
  for i = 1, 4 do
    local g = CreateFrame("Frame", nil, root, "BackdropTemplate")
    g:SetFrameLevel(9)
    g:SetBackdrop({
      edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
      edgeSize = 12,
      insets = { left = 2, right = 2, top = 2, bottom = 2 },
    })
    g:SetBackdropBorderColor(C.accent[1], C.accent[2], C.accent[3], 0.9)
    g:SetBackdropColor(0, 0, 0, 0)
    g:EnableMouse(false)
    g:Hide()
    alsoGlows[i] = g
  end

  -- Coach card
  card = CreateFrame("Frame", "SpellSmasherTourCard", root, "BackdropTemplate")
  card:SetFrameLevel(20)
  card:SetSize(400, 420)
  card:EnableMouse(true)
  card:SetMovable(true)
  card:RegisterForDrag("LeftButton")
  card:SetScript("OnDragStart", card.StartMoving)
  card:SetScript("OnDragStop", card.StopMovingOrSizing)
  if card.SetBackdrop then
    card:SetBackdrop({
      bgFile = "Interface\\Buttons\\WHITE8x8",
      edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
      edgeSize = 16,
      insets = { left = 4, right = 4, top = 4, bottom = 4 },
    })
    card:SetBackdropColor(0.031, 0.039, 0.063, 0.97)
    card:SetBackdropBorderColor(C.green[1], C.green[2], C.green[3], 1)
  end

  progFS = card:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
  progFS:SetPoint("TOPRIGHT", -14, -12)
  SetFS(progFS, C.muted)

  badgeFS = card:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
  badgeFS:SetPoint("TOPLEFT", 14, -12)
  SetFS(badgeFS, C.accent)

  titleFS = card:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
  titleFS:SetPoint("TOPLEFT", 14, -32)
  titleFS:SetPoint("RIGHT", -14, 0)
  titleFS:SetJustifyH("LEFT")
  SetFS(titleFS, C.green)

  -- Progress track
  local track = CreateFrame("Frame", nil, card, "BackdropTemplate")
  track:SetPoint("TOPLEFT", 14, -56)
  track:SetPoint("TOPRIGHT", -14, -56)
  track:SetHeight(6)
  track.bg = track:CreateTexture(nil, "BACKGROUND")
  track.bg:SetAllPoints()
  track.bg:SetColorTexture(0.1, 0.12, 0.18, 1)
  progFill = track:CreateTexture(nil, "ARTWORK")
  progFill:SetPoint("TOPLEFT")
  progFill:SetPoint("BOTTOMLEFT")
  progFill:SetWidth(40)
  progFill:SetColorTexture(C.green[1], C.green[2], C.green[3], 1)

  local scroll = CreateFrame("ScrollFrame", nil, card)
  scroll:SetPoint("TOPLEFT", 12, -70)
  scroll:SetPoint("BOTTOMRIGHT", -12, 56)
  local child = CreateFrame("Frame", nil, scroll)
  child:SetSize(360, 800)
  scroll:SetScrollChild(child)
  scroll:EnableMouseWheel(true)
  scroll:SetScript("OnMouseWheel", function(self, delta)
    local cur = self:GetVerticalScroll() or 0
    local maxS = math.max(0, (child:GetHeight() or 0) - (self:GetHeight() or 1))
    self:SetVerticalScroll(math.max(0, math.min(maxS, cur - delta * 36)))
  end)

  bodyFS = child:CreateFontString(nil, "OVERLAY", "GameFontNormal")
  bodyFS:SetPoint("TOPLEFT", 2, -2)
  bodyFS:SetWidth(352)
  bodyFS:SetJustifyH("LEFT")
  bodyFS:SetJustifyV("TOP")
  bodyFS:SetSpacing(3)
  SetFS(bodyFS, C.text)

  card.scroll = scroll
  card.child = child
  card.bodyFS = bodyFS

  local hint = card:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
  hint:SetPoint("BOTTOMLEFT", 14, 40)
  hint:SetPoint("BOTTOMRIGHT", -14, 40)
  hint:SetJustifyH("LEFT")
  hint:SetText("Skip / Esc = stop. Help restarts. Drag card if needed.")
  SetFS(hint, C.muted)

  -- Buttons
  local function MakeBtn(parent, text, kind, onClick)
    local b = CreateFrame("Button", nil, parent, "UIPanelButtonTemplate")
    b:SetSize(88, 26)
    b:SetText(text)
    b:SetScript("OnClick", onClick)
    return b
  end

  -- Prefer themed buttons if UI already created MakeTheme-like; use simple panel buttons
  -- so Tour loads even if UI not ready. Styling via backdrop.
  -- NOTE: do not name the frame `b` while also taking blue as `b` (shadows color args).
  local function ThemeishBtn(parent, w, text, cr, cg, cb, onClick)
    local btn = CreateFrame("Button", nil, parent, "BackdropTemplate")
    btn:SetSize(w, 28)
    btn:SetBackdrop({
      bgFile = "Interface\\Buttons\\WHITE8x8",
      edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
      edgeSize = 10,
      insets = { left = 2, right = 2, top = 2, bottom = 2 },
    })
    local rr = tonumber(cr) or 0.2
    local gg = tonumber(cg) or 0.25
    local bb = tonumber(cb) or 0.35
    btn:SetBackdropColor(rr, gg, bb, 0.9)
    btn:SetBackdropBorderColor(0.2, 0.25, 0.35, 1)
    btn:EnableMouse(true)
    btn:RegisterForClicks("LeftButtonUp")
    btn:SetScript("OnClick", onClick)
    local fs = btn:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    fs:SetPoint("CENTER")
    fs:SetText(text)
    if rr > 0.3 and gg > 0.5 then
      fs:SetTextColor(0.02, 0.14, 0.09)
    else
      fs:SetTextColor(C.text[1], C.text[2], C.text[3])
    end
    btn.fs = fs
    btn.SetLabel = function(self, t)
      self.fs:SetText(t)
    end
    return btn
  end

  card.btnSkip = ThemeishBtn(card, 72, "Skip", 0.15, 0.18, 0.25, function()
    Tour.Stop(true, "Tour skipped - |cffffff00Help|r restarts anytime")
  end)
  card.btnSkip:SetPoint("BOTTOMLEFT", 12, 10)

  card.btnBack = ThemeishBtn(card, 72, "Back", 0.2, 0.25, 0.4, function()
    Tour.Go(pageIndex - 1)
  end)
  card.btnBack:SetPoint("RIGHT", card, "BOTTOMRIGHT", -100, 24)

  card.btnNext = ThemeishBtn(card, 88, "Next", C.green[1], C.green[2], C.green[3], function()
    if pageIndex >= #ActiveSteps() then
      Tour.Stop(true, "Tour finished - |cffffff00Help|r restarts anytime")
    else
      Tour.Go(pageIndex + 1)
    end
  end)
  card.btnNext:SetPoint("BOTTOMRIGHT", -12, 10)

  -- Esc via UISpecialFrames pattern: listen OnKeyDown on card when active
  card:SetScript("OnKeyDown", function(self, key)
    if not active then
      return
    end
    if key == "ESCAPE" then
      Tour.Stop(true, "Tour closed - |cffffff00Help|r restarts")
    elseif key == "RIGHT" or key == "ENTER" then
      if pageIndex >= #ActiveSteps() then
        Tour.Stop(true, "Tour finished - |cffffff00Help|r restarts anytime")
      else
        Tour.Go(pageIndex + 1)
      end
    elseif key == "LEFT" then
      Tour.Go(pageIndex - 1)
    end
  end)
  if card.SetPropagateKeyboardInput then
    card:SetPropagateKeyboardInput(false)
  end
end

---------------------------------------------------------------------------
-- Layout dim hole + glow + card
---------------------------------------------------------------------------
local function HideDimParts()
  for _, p in pairs(dimParts) do
    if p then
      p:Hide()
      p:ClearAllPoints()
    end
  end
  glow:Hide()
  glow:ClearAllPoints()
  for _, g in ipairs(alsoGlows) do
    g:Hide()
    g:ClearAllPoints()
  end
end

local function PlaceGlowOn(frame, glowFrame, pad)
  local l, b, w, h = GetRectSafe(frame)
  if not l then
    glowFrame:Hide()
    return false
  end
  l, b, w, h = PadRect(l, b, w, h, pad or 5)
  glowFrame:ClearAllPoints()
  glowFrame:SetPoint("BOTTOMLEFT", UIParent, "BOTTOMLEFT", l, b)
  glowFrame:SetSize(w, h)
  glowFrame:Show()
  return true, l, b, w, h
end

local function LayoutDimAround(l, b, w, h)
  -- Full screen dims with hole [l,b,w,h]
  local uiW = UIParent:GetWidth() or 1920
  local uiH = UIParent:GetHeight() or 1080
  dimParts.full:Hide()

  -- top
  dimParts.t:ClearAllPoints()
  dimParts.t:SetPoint("TOPLEFT", UIParent, "BOTTOMLEFT", 0, uiH)
  dimParts.t:SetPoint("BOTTOMRIGHT", UIParent, "BOTTOMLEFT", uiW, b + h)
  dimParts.t:Show()

  -- bottom
  dimParts.b:ClearAllPoints()
  dimParts.b:SetPoint("TOPLEFT", UIParent, "BOTTOMLEFT", 0, b)
  dimParts.b:SetPoint("BOTTOMRIGHT", UIParent, "BOTTOMLEFT", uiW, 0)
  dimParts.b:Show()

  -- left (middle strip only)
  dimParts.l:ClearAllPoints()
  dimParts.l:SetPoint("TOPLEFT", UIParent, "BOTTOMLEFT", 0, b + h)
  dimParts.l:SetPoint("BOTTOMRIGHT", UIParent, "BOTTOMLEFT", l, b)
  dimParts.l:Show()

  -- right
  dimParts.r:ClearAllPoints()
  dimParts.r:SetPoint("TOPLEFT", UIParent, "BOTTOMLEFT", l + w, b + h)
  dimParts.r:SetPoint("BOTTOMRIGHT", UIParent, "BOTTOMLEFT", uiW, b)
  dimParts.r:Show()
end

local function LayoutFullDim()
  dimParts.t:Hide()
  dimParts.b:Hide()
  dimParts.l:Hide()
  dimParts.r:Hide()
  dimParts.full:ClearAllPoints()
  dimParts.full:SetAllPoints(UIParent)
  dimParts.full:Show()
  glow:Hide()
end

local function PlaceCard(place, holeL, holeB, holeW, holeH)
  card:ClearAllPoints()
  local cw, ch = card:GetWidth() or 400, card:GetHeight() or 420
  local uiW = UIParent:GetWidth() or 1920
  local uiH = UIParent:GetHeight() or 1080
  place = place or "side"

  if place == "center" or not holeL then
    card:SetPoint("CENTER", UIParent, "CENTER", 0, 20)
    return
  end

  local main = hooks.getMain and hooks.getMain()
  -- Prefer right of main window if it fits
  if main and main:IsVisible() then
    local ml, mb, mw, mh = GetRectSafe(main)
    if ml and (ml + mw + cw + 24) < uiW then
      card:SetPoint("TOPLEFT", UIParent, "BOTTOMLEFT", ml + mw + 12, mb + mh)
      return
    end
    if ml and (ml - cw - 12) > 0 then
      card:SetPoint("TOPRIGHT", UIParent, "BOTTOMLEFT", ml - 12, mb + mh)
      return
    end
  end

  if place == "bottom" and holeL then
    local x = math.max(12, math.min(uiW - cw - 12, holeL + holeW / 2 - cw / 2))
    local y = math.max(12, holeB - ch - 16)
    card:SetPoint("BOTTOMLEFT", UIParent, "BOTTOMLEFT", x, y)
    return
  end

  -- side of hole: right if room else left else center
  if holeL + holeW + cw + 20 < uiW then
    card:SetPoint("TOPLEFT", UIParent, "BOTTOMLEFT", holeL + holeW + 14, holeB + holeH)
  elseif holeL - cw - 14 > 0 then
    card:SetPoint("TOPRIGHT", UIParent, "BOTTOMLEFT", holeL - 14, holeB + holeH)
  else
    card:SetPoint("CENTER", UIParent, "CENTER", 0, 20)
  end
end

local function ResolveTargetFrame(id)
  if not id then
    return nil
  end
  local fr = targets[id]
  if fr and fr.IsVisible and fr:IsVisible() then
    return fr
  end
  -- Dynamic list targets from UI
  local ui = hooks.getUI and hooks.getUI()
  if ui then
    if id == "listUse" and ui.tourUseBtn then
      return ui.tourUseBtn
    end
    if id == "listEdit" and ui.tourEditBtn then
      return ui.tourEditBtn
    end
    if id == "stepMode" and ui.tourModePill then
      return ui.tourModePill
    end
    if id == "stepList" and ui.stepScroll then
      return ui.stepScroll
    end
  end
  return fr
end

local function LayoutTour()
  if not active or not root then
    return
  end
  local step = ActiveSteps()[pageIndex]
  if not step then
    return
  end

  HideDimParts()

  local primary = ResolveTargetFrame(step.target)
  local holeL, holeB, holeW, holeH
  if primary then
    local ok, l, b, w, h = PlaceGlowOn(primary, glow, 6)
    if ok then
      holeL, holeB, holeW, holeH = l, b, w, h
      LayoutDimAround(l, b, w, h)
    else
      LayoutFullDim()
    end
  else
    LayoutFullDim()
  end

  -- also glows
  if type(step.also) == "table" then
    local gi = 1
    for _, aid in ipairs(step.also) do
      if gi > #alsoGlows then
        break
      end
      local fr = ResolveTargetFrame(aid)
      if fr and fr ~= primary then
        if PlaceGlowOn(fr, alsoGlows[gi], 4) then
          gi = gi + 1
        end
      end
    end
  end

  PlaceCard(step.place, holeL, holeB, holeW, holeH)
end

---------------------------------------------------------------------------
-- Render page content
---------------------------------------------------------------------------
local function BuildBodyText(step)
  local t = tostring(step.body or "")
  if type(step.bullets) == "table" and #step.bullets > 0 then
    t = t .. "\n\n|cff6c8cffKey points|r\n"
    for _, b in ipairs(step.bullets) do
      t = t .. "|cff3dd68c*|r " .. tostring(b) .. "\n"
    end
  end
  if step.note and step.note ~= "" then
    t = t .. "\n|cff8b96ad" .. tostring(step.note) .. "|r"
  end
  return t
end

local function RenderPage()
  EnsureRoot()
  local step = ActiveSteps()[pageIndex]
  if not step then
    return
  end

  -- Tab switch
  if step.tab and type(hooks.showTab) == "function" then
    SafeCall(hooks.showTab, step.tab)
  end

  if step.ensureSample then
    EnsureSamplePack()
    if type(hooks.refresh) == "function" then
      SafeCall(hooks.refresh)
    end
  end

  badgeFS:SetText("Tour " .. Tour.VERSION_LABEL)
  progFS:SetText(pageIndex .. " / " .. #ActiveSteps())
  titleFS:SetText(step.title or ("Step " .. pageIndex))

  local frac = pageIndex / #ActiveSteps()
  local trackW = (card:GetWidth() or 400) - 28
  progFill:SetWidth(math.max(8, trackW * frac))

  local text = BuildBodyText(step)
  bodyFS:SetText(text)
  local bh = bodyFS:GetStringHeight() or 200
  card.child:SetHeight(math.max(200, bh + 24))
  card.scroll:SetVerticalScroll(0)

  if card.btnBack then
    if pageIndex <= 1 then
      card.btnBack:Disable()
      card.btnBack:SetAlpha(0.45)
    else
      card.btnBack:Enable()
      card.btnBack:SetAlpha(1)
    end
  end
  if card.btnNext and card.btnNext.SetLabel then
    card.btnNext:SetLabel(step.cta or (pageIndex >= #ActiveSteps() and "Finish" or "Next"))
  end

  -- Layout after tab/show settles
  local function doLayout()
    if active then
      LayoutTour()
    end
  end
  if C_Timer and C_Timer.After then
    C_Timer.After(0.05, doLayout)
    C_Timer.After(0.2, doLayout)
  else
    doLayout()
  end

  if type(hooks.setStatus) == "function" then
    hooks.setStatus(
      "|cff3dd68cTour|r step "
        .. pageIndex
        .. " / "
        .. #ActiveSteps()
        .. "  -  Next / Back / Skip / Esc"
    )
  end
end

function Tour.Go(page)
  page = tonumber(page) or 1
  if page < 1 then
    page = 1
  end
  if page > #ActiveSteps() then
    page = #ActiveSteps()
  end
  pageIndex = page
  RenderPage()
end

function Tour.Start(page)
  EnsureRoot()
  active = true
  pageIndex = tonumber(page) or 1
  if pageIndex < 1 then
    pageIndex = 1
  end
  root:Show()
  card:Show()
  -- Keyboard on root (Esc/arrows) - works without focus-fighting EditBoxes
  root:EnableKeyboard(true)
  card:EnableKeyboard(true)
  if root.SetPropagateKeyboardInput then
    root:SetPropagateKeyboardInput(false)
  end
  if card.SetPropagateKeyboardInput then
    card:SetPropagateKeyboardInput(false)
  end
  root:SetScript("OnKeyDown", function(_, key)
    if not active then
      return
    end
    if key == "ESCAPE" then
      Tour.Stop(true, "Tour closed - |cffffff00Tutorial|r restarts")
    elseif key == "RIGHT" or key == "ENTER" then
      if pageIndex >= #ActiveSteps() then
        Tour.Stop(true, "Tour finished - |cffffff00Tutorial|r restarts anytime")
      else
        Tour.Go(pageIndex + 1)
      end
    elseif key == "LEFT" then
      Tour.Go(pageIndex - 1)
    end
  end)
  -- Open main UI if needed
  if hooks.openMain then
    SafeCall(hooks.openMain)
  end
  RenderPage()
  Tour.SyncHelpCta(helpCtaBtn)
  -- Layout ticker for UI scale / drag main frame / list rebuild
  if layoutTicker then
    layoutTicker:Cancel()
    layoutTicker = nil
  end
  if C_Timer and C_Timer.NewTicker then
    layoutTicker = C_Timer.NewTicker(0.35, function()
      if not active then
        return
      end
      -- Refresh dynamic list/edit targets each tick (rows rebuild on Refresh)
      local ui = hooks.getUI and hooks.getUI()
      if ui then
        if ui.tourUseBtn then
          targets.listUse = ui.tourUseBtn
        end
        if ui.tourEditBtn then
          targets.listEdit = ui.tourEditBtn
        end
        if ui.tourModePill then
          targets.stepMode = ui.tourModePill
        end
        if ui.stepScroll then
          targets.stepList = ui.stepScroll
        end
      end
      LayoutTour()
    end)
  end
end

function Tour.Stop(markDone, statusMsg)
  active = false
  if layoutTicker then
    layoutTicker:Cancel()
    layoutTicker = nil
  end
  if markDone then
    Tour.MarkDone()
  end
  if root then
    root:EnableKeyboard(false)
    root:SetScript("OnKeyDown", nil)
    root:Hide()
  end
  HideDimParts()
  if card then
    card:EnableKeyboard(false)
  end
  Tour.SyncHelpCta(helpCtaBtn)
  if type(hooks.setStatus) == "function" then
    hooks.setStatus(statusMsg or "Tour closed - |cffffff00Help|r or |cffffff00/ssm help|r restarts")
  end
end

function Tour.MaybeAutoShow()
  if Tour.IsDone() then
    return
  end
  if C_Timer and C_Timer.After then
    C_Timer.After(0.4, function()
      if Tour.IsDone() then
        return
      end
      local main = hooks.getMain and hooks.getMain()
      if main and main:IsShown() and not active then
        Tour.Start(1)
      end
    end)
  end
end

---------------------------------------------------------------------------
-- Help CTA flash / Replay (theme parity)
---------------------------------------------------------------------------
function Tour.SyncHelpCta(btn)
  helpCtaBtn = btn or helpCtaBtn
  btn = helpCtaBtn
  if not btn then
    return
  end
  if active then
    -- Quiet while tour open
    if btn.SetThemeText then
      btn:SetThemeText("Help")
    elseif btn.SetText then
      btn:SetText("Help")
    end
    btn:SetAlpha(1)
    return
  end
  local done = Tour.IsDone()
  if btn.SetThemeText then
    btn:SetThemeText(done and "Replay" or "Tutorial")
  elseif btn.SetText then
    btn:SetText(done and "Replay" or "Tutorial")
  end
  if done then
    btn:SetAlpha(0.92)
    if btn._tourFlash then
      btn._tourFlash = false
      btn:SetScript("OnUpdate", nil)
    end
  else
    -- Flash until finished once (this tour version)
    if not btn._tourFlash then
      btn._tourFlash = true
      local t = 0
      btn:SetScript("OnUpdate", function(self, elapsed)
        if Tour.IsDone() or active then
          self:SetAlpha(1)
          self:SetScript("OnUpdate", nil)
          self._tourFlash = false
          return
        end
        t = t + (elapsed or 0.016)
        local a = 0.55 + 0.45 * (0.5 + 0.5 * math.sin(t * 4))
        self:SetAlpha(a)
      end)
    end
  end
end

function Tour.GetStepCount()
  return #ActiveSteps()
end

return Tour
