--[[
  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 v20).
Tour.VERSION = 1
Tour.VERSION_LABEL = "v1"

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 - Full product tour",
    body = "This tour walks SpellSmasher |cff3dd68clike the website Tutorial|r: spotlight on real controls, Next in order, full happy path.\n\n"
      .. "You mash |cffffff00one key|r. The addon only casts spells |cffffff00on your pack list|r.\n"
      .. "Not Blizzard's adaptive Single-Button Assist.\n\n"
      .. "|cff6c8cffPath|r List > New pack > Import > Edit > modes > helpers > talents > bind > mash.\n\n"
      .. "Skip anytime. Help restarts. After Finish, Help becomes quiet Replay until tour version changes.",
    bullets = {
      "Spotlight hole + green glow = look here",
      "Next / Back · Esc or Skip closes",
      "Sample pack created if your list is empty (Method for your class)",
    },
    tab = "list",
    target = "help",
    also = { "tabList" },
    place = "center",
    cta = "Start tour",
  },
  {
    id = "list",
    title = "Step 2 - List (your packs)",
    body = "Home for all packs (account-wide SavedVariables).\n\n"
      .. "|cff6c8cffWhat to do|r\n"
      .. "1) Find a pack row (or create one next).\n"
      .. "2) Click |cff3dd68cUse|r so it shows |cffffff00* ACTIVE|r - that pack drives mash.\n"
      .. "3) Optional: |cffffff00This class|r filter hides other classes.\n"
      .. "4) |cff3dd68cExport selected|r / Export all for backup or spellsmasher.com.\n"
      .. "5) Rename / Dup / Delete on a row (Dup = deep copy only).",
    bullets = {
      "Active = mash · selection = export",
      "Import appends - never wipes your list",
      "Empty list is normal first install",
    },
    tab = "list",
    target = "listHead",
    also = { "listFilter", "exportSelected", "newFromList" },
    place = "side",
  },
  {
    id = "newpack",
    title = "Step 3 - New pack",
    body = "Build a starter list for your class / spec / hero.\n\n"
      .. "|cff6c8cffWhat to do|r\n"
      .. "1) Class + spec (synced to character when possible).\n"
      .. "2) Level is read from this character (70-90) and stamped on the pack.\n"
      .. "3) Hero talent chip (2 Midnight paths per spec).\n"
      .. "4) |cffffff00Create Method pack|r (guide order + talent CUk) or |cffffff00Create Engine pack|r (CD-aware order, same talents).\n"
      .. "5) Pack is |cff3dd68cappended|r - never wipes existing packs.\n\n"
      .. "|cfff0b429Note:|r Method/Engine lists are |cffffff00full starters|r (not level-trimmed). Level-legal spell lists: |cff3dd68cspellsmasher.com|r.",
    bullets = {
      "Two mash keys? Make two packs later",
      "Hero chip locks which Method/Engine package you get",
      "Castables only - passives are not sequence steps",
    },
    tab = "new",
    target = "methodCreate",
    also = { "engineCreate", "heroHost", "classHost" },
    place = "side",
  },
  {
    id = "import",
    title = "Step 4 - Import from website",
    body = "Bring packs from |cff3dd68cspellsmasher.com|r (or a friend backup).\n\n"
      .. "|cff6c8cffWhat to do|r\n"
      .. "1) Site: build/edit > green |cffffff00Export SpellSmasher pack|r > Copy.\n"
      .. "2) Paste the whole string here (must start with |cff3dd68c!SpellSmasher!|r).\n"
      .. "3) Click |cff3dd68cImport|r - packs are |cffffff00added|r, not wiped.\n"
      .. "4) List > |cff3dd68cUse|r > bind mash key.\n\n"
      .. "Full website walkthrough with real class choices: Help on the site > Complete sample (Vengeance).",
    bullets = {
      "String must stay complete (no cut start/end)",
      "Optional third-party wire is not required",
      "Export from List fills this box - Ctrl+C",
    },
    tab = "import",
    target = "importBtn",
    also = { "importBox", "exportImp" },
    place = "side",
  },
  {
    id = "sample",
    title = "Step 5 - Sample pack ready",
    body = "If you had no pack, SpellSmasher just ensured a |cff3dd68cMethod sample|r for your class/spec (or selected an existing one) so Edit is not empty.\n\n"
      .. "Open it with List > |cffffff00Edit|r (tour will switch to Edit next).\n\n"
      .. "You can delete sample packs anytime. Import from the site anytime for level-legal lists.",
    bullets = {
      "Sample = full Method starter (not level-trimmed)",
      "Site export = level-honest spells",
      "Active pack = what you mash",
    },
    tab = "list",
    target = "listUse",
    also = { "listEdit", "listHead" },
    place = "side",
    ensureSample = true,
  },
  {
    id = "editor",
    title = "Step 6 - Edit tab",
    body = "You are on |cff3dd68cEdit|r - one pack's spell list and talents.\n\n"
      .. "|cff6c8cffWhat to do|r\n"
      .. "1) Pack name at top - type + Enter to rename.\n"
      .. "2) Spell rows below = the fixed list mash uses.\n"
      .. "3) Footer |cff3dd68cSave|r writes and recompiles the mash button.\n"
      .. "4) List |cff3dd68cUse|r still picks which pack is Active in combat.",
    bullets = {
      "Save after edits (or Recompile top bar)",
      "Export from List for backup string",
      "Passives never invent /cast lines",
    },
    tab = "editor",
    target = "packName",
    also = { "saveBtn", "editHeader" },
    place = "side",
    ensureSample = true,
  },
  {
    id = "modes",
    title = "Step 7 - Spell modes",
    body = "Each spell has a mode (click the mode pill on a row to cycle).\n\n"
      .. "|cffffff00Once (per combat)|r - fires once this fight, then SKIPPED until leave combat.\n"
      .. "|cffffff00In order|r - recipe top to bottom every pass.\n"
      .. "|cffffff00Priority|r - main spam; top rows more often (triangle). % = share.\n"
      .. "|cffffff00Rev Pri|r - reverse triangle (rare).\n\n"
      .. "Typical: Once openers + short In order, then long Priority.",
    bullets = {
      "Priority top = tried more often",
      "Once is not once-per-loop",
      "Order matters for Priority",
    },
    tab = "editor",
    target = "stepMode",
    also = { "stepList", "addSpell" },
    place = "side",
    ensureSample = true,
  },
  {
    id = "rows",
    title = "Step 8 - Row tools",
    body = "Tools on each spell row (and footer fields for the selected row).\n\n"
      .. "|cff6c8cffWhat to do|r\n"
      .. "- ^ v reorder (Priority cares about order).\n"
      .. "- - / + and xN = run this spell N times in its block (Once ignores xN).\n"
      .. "- Mode pill cycles Once > In order > Priority > Rev Pri.\n"
      .. "- Footer Name / Macro + Apply for selected row.\n"
      .. "- + Spell add | delete remove (asks first).\n"
      .. "- Rebuild reloads Method/Engine list (overwrites custom steps).",
    bullets = {
      "Save or Recompile after changes",
      "Macro lines stay under 255 chars",
      "CastGuard blocks pure passives",
    },
    tab = "editor",
    target = "stepList",
    also = { "addSpell", "rebuildBtn", "stepApply" },
    place = "side",
    ensureSample = true,
  },
  {
    id = "helpers",
    title = "Step 9 - Helpers + Shift",
    body = "Bottom bar helpers (any tab). |cffffff00Off by default|r.\n\n"
      .. "- |cff3dd68cAA|r - auto-attack comfort line (if it fits 255 chars).\n"
      .. "- |cff3dd68cRT|r - re-target when useful.\n"
      .. "- |cff3dd68cDEF|r - defensive helper only if not already on the list.\n"
      .. "- |cffffff00Shift|r - ignore (default) / aoe / pause while holding Shift.\n\n"
      .. "Helpers never invent secret combat spells. |cffffff00Your list is the only cast source.|r",
    bullets = {
      "Turn On only what you want",
      "Interrupt helpers need the kick on the list when designed that way",
      "Mash always advances the list",
    },
    tab = "list",
    target = "helperAA",
    also = { "helperRT", "helperDEF", "shiftBtn", "keyBar" },
    place = "bottom",
  },
  {
    id = "talents",
    title = "Step 10 - Talents (Apply)",
    body = "Edit tab talent zone - Method CUk on the pack.\n\n"
      .. "|cff6c8cffOne-click Apply|r\n"
      .. "Press |cff3dd68cApply|r only. Auto Best/Method if needed, then spends |cffffff00your|r points along the Method package.\n\n"
      .. "|cfff0b429Important:|r Apply |cffffff00overwrites the loadout already selected|r in Talents (N). It does |cffffff00not|r create a new named loadout.\n\n"
      .. "Level up and Apply again for more. Full Method needs max points.\n"
      .. "Under max: Apply/Copy scale |cffffff00talents|r - they do |cffffff00not|r auto-trim the spell list.\n\n"
      .. "|cffffff00From me|r = YOUR live tree. |cffffff00Copy|r = new loadout name via Blizzard Import.",
    bullets = {
      "Mash never changes talents by itself",
      "Out of combat for Apply",
      "Best attaches package for this list (preview)",
    },
    tab = "editor",
    target = "talentApply",
    also = { "talentBest", "talentCopy", "talentFromMe", "talentZone" },
    place = "side",
    ensureSample = true,
  },
  {
    id = "bind",
    title = "Step 11 - Bind + mash + export",
    body = "Finish setup and play.\n\n"
      .. "|cff6c8cffBind|r\n"
      .. "Mash key bar: click |cffffff001-0|r, or Esc > Options > Key Bindings > SpellSmasher.\n\n"
      .. "|cff6c8cffMash|r\n"
      .. "Only the |cffffff00* ACTIVE|r pack runs. One combat cast per click (+ helpers if On).\n\n"
      .. "|cff6c8cffExport|r\n"
      .. "Export selected / all > Ctrl+C. May attach live talents when class|spec matches.\n"
      .. "Does |cffffff00not|r re-trim spells or rewrite pack level. Level-legal lists: site.\n"
      .. "If pack level differs from live, status says so (list unchanged).",
    bullets = {
      "Active != export selection",
      "site = level-legal authoring",
      "addon = play + bridge + talent scale",
    },
    tab = "list",
    target = "keyBar",
    also = { "exportSelected", "listUse" },
    place = "bottom",
  },
  {
    id = "done",
    title = "Step 12 - Done - go play",
    body = "You walked the full in-game path:\n"
      .. "List > New / Import > Edit > modes > helpers > Apply talents > bind > mash.\n\n"
      .. "Website sample (every click with real class names):\n"
      .. "spellsmasher.com/help/ > Complete sample: Vengeance Demon Hunter.\n\n"
      .. "Restart: |cffffff00Help|r or |cffffff00/ssm help|r. After Finish, Help stays as quiet Replay until tour content version bumps.",
    bullets = {
      "Happy path: Method pack > Use > bind > Apply > mash",
      "Apply overwrites current N loadout",
      "You still press the key - have fun",
    },
    tab = "list",
    target = "help",
    also = { "tabList" },
    place = "center",
    cta = "Finish",
  },
}

---------------------------------------------------------------------------
-- 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 >= #STEPS 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 >= #STEPS 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 = STEPS[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 = STEPS[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 .. " / " .. #STEPS)
  titleFS:SetText(step.title or ("Step " .. pageIndex))

  local frac = pageIndex / #STEPS
  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 >= #STEPS 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
        .. " / "
        .. #STEPS
        .. "  -  Next / Back / Skip / Esc"
    )
  end
end

function Tour.Go(page)
  page = tonumber(page) or 1
  if page < 1 then
    page = 1
  end
  if page > #STEPS then
    page = #STEPS
  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 >= #STEPS 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 #STEPS
end

return Tour
