/* global React, window, document, Icon */
// Guided demo tour. Walks the user through the live cockpit UX. Each step
// targets a `[data-tour="…"]` element. Steps with `awaitNext: true`
// auto-advance after the spotlit target is clicked, either by the user or
// by the tour's Next button, giving the next surface time to mount.

const AE_TOUR_STEPS = [
  {
    id: "mode-toggle",
    target: '[data-tour="mode-toggle"]',
    title: "Switch roles for the demo",
    body: "This AE / MGR toggle is here for demo purposes. It lets you show the same buyer-proof system from the rep's daily cockpit or the manager's coaching and forecast lens.",
  },
  {
    id: "verdict",
    target: '[data-tour="verdict"]',
    title: "The verdict opens the day",
    body: "Pipeline isn't judged by activity. The first sentence the AE sees names how much pipeline lacks buyer proof.",
  },
  {
    id: "priority-select",
    target: '[data-tour="priority-select"]',
    title: "Pick the deal that needs proof",
    body: "The left rail ranks deals by buyer-proof risk. Select the top deal and the right pane turns into a diagnosis, not a generic task list.",
    awaitNext: true,
    hint: "Next selects the first priority deal.",
  },
  {
    id: "priority-detail",
    target: '[data-tour="priority-detail"]',
    title: "Diagnosis before motion",
    body: "The detail pane explains why this deal is priority: the missing buyer signal, the seller-buyer gap, and the proof points that matter.",
  },
  {
    id: "priority-cta",
    target: '[data-tour="priority-cta"]',
    title: "One recommended move",
    body: "The CTA is not just a reminder. It is the next buyer-facing move Nudge believes can create proof and firm the forecast.",
    awaitNext: true,
    hint: "Next opens the recommended move preview.",
  },
  {
    id: "action-preview",
    target: '[data-tour="action-preview"]',
    title: "Preview before send",
    body: "The draft, recipient, evidence, and expected firmness are visible before anything leaves Nudge. The AE stays in control while Addy does the prep.",
    hint: "Open the action preview to continue.",
  },
  {
    id: "action-impact",
    target: '[data-tour="action-impact"]',
    title: "Forecast impact is explicit",
    body: "Every move is tied back to buyer-seller gap and forecast firmness, so the AE sees why this action matters now.",
    hint: "Open the action preview to continue.",
  },
];

const MGR_TOUR_STEPS = [
  {
    id: "mode-toggle",
    target: '[data-tour="mode-toggle"]',
    title: "Switch roles for the demo",
    body: "This AE / MGR toggle is here for demo purposes. It lets you show the same buyer-proof system from the rep's daily cockpit or the manager's coaching and forecast lens.",
  },
  {
    id: "verdict",
    target: '[data-tour="verdict"]',
    title: "Team proof opens the day",
    body: "The manager sees the same buyer-proof verdict as the AE, but across the team pipeline: where seller activity is ahead of buyer movement.",
  },
  {
    id: "priority-select",
    target: '[data-tour="priority-select"]',
    title: "Start with the coaching queue",
    body: "The ranked list is now team-scoped. Pick the first deal to see which rep needs coaching and what evidence is missing.",
    awaitNext: true,
    hint: "Next selects the first priority deal.",
  },
  {
    id: "priority-detail",
    target: '[data-tour="priority-detail"]',
    title: "Inspect buyer reality",
    body: "The right pane names the risk in manager language: buyer movement, gap size, and the proof the AE needs before the forecast call.",
  },
  {
    id: "priority-cta",
    target: '[data-tour="priority-cta"]',
    title: "Coach the rep, not the buyer",
    body: "In MGR mode, the action becomes a coaching move to the AE. The manager asks for buyer-owned proof instead of sending the buyer message themselves.",
    awaitNext: true,
    hint: "Next opens the coaching move preview.",
  },
  {
    id: "action-preview",
    target: '[data-tour="action-preview"]',
    title: "Manager preview",
    body: "The preview is a Slack-style coaching note to the rep with the exact challenge, deal context, and evidence behind the ask.",
    hint: "Open the action preview to continue.",
  },
  {
    id: "action-impact",
    target: '[data-tour="action-impact"]',
    title: "Coaching tied to forecast firmness",
    body: "The manager sees expected firmness and gap movement, so coaching is connected to forecast quality instead of activity inspection.",
    hint: "Open the action preview to continue.",
  },
];

/* AE deal-room tour — opens a deal in AE mode and walks through the
   Deal Truth pane's anatomy: defense card → lens switch → context
   disclosure → CTA → Next move tab. */
const AE_DEAL_ROOM_STEPS = [
  {
    id: "deal-defense",
    target: '[data-tour="defense"]',
    title: "Deal Truth opens the room",
    body: "The first card names the buyer reality of the deal — what's true right now, in one read, before any motion.",
  },
  {
    id: "defense-categories",
    target: '[data-tour="defense-categories"]',
    title: "Gap + action, tagged",
    body: "Every deal carries two chips: the buyer-side gap (champion drift, missing stakeholder, process blocker, value proof, timing risk) and the recommended action (re-engage, map people, unblock, prove value, close or expand). Same taxonomy across cockpit, deals list, and forecast — so one deal reads consistently everywhere.",
  },
  {
    id: "defense-lens-switch",
    target: '[data-tour="defense-lens-switch"]',
    title: "Switch the lens",
    body: "Same deal, three reads. Full story shows the truth. Seller story shows what the rep has done. Buyer story shows what the buyer has done back.",
  },
  {
    id: "defense-context",
    target: '[data-tour="defense-context"]',
    title: "Deal context on demand",
    body: "Tap Deal context to unfold the buyer↔seller alignment bar, missing proof, and the buying committee. The card stays calm until you want the detail.",
    awaitNext: true,
    hint: "Next opens the context drawer.",
  },
  {
    id: "defense-cta",
    target: '[data-tour="defense-cta"]',
    title: "One recommended move",
    body: "The CTA is the next buyer-facing action Nudge believes will create proof and firm the forecast — drafted, channel-aware, ready to preview.",
    awaitNext: true,
    hint: "Next jumps to the action.",
  },
  {
    id: "next-move-tab",
    target: '[data-tour="next-move-tab"]',
    title: "Next move tab",
    body: "Every action lives in this tab — drafted message, recipient, evidence, expected firmness gain. The AE stays in control while Addy does the prep.",
  },
  {
    id: "more-tab",
    target: '[data-tour="more-tab"]',
    title: "More tabs live behind the menu",
    body: "Less-frequent surfaces — Notes, Edit deal, Activity, Stakeholders, Meetings, Resources — tuck behind a dropdown to keep the room calm.",
    awaitNext: true,
    hint: "Next opens the menu.",
  },
  {
    id: "more-activity",
    target: '[data-tour="more-activity"]',
    title: "Example: Activity",
    body: "Pick a tab from the menu — here, Activity. The room swaps content without leaving the deal context.",
    awaitNext: true,
    hint: "Next opens Activity.",
  },
  {
    id: "activity-tab",
    target: '[data-tour="activity-tab"]',
    title: "Activity timeline",
    body: "Every buyer reply, seller move, meeting, note, and Nudge-dispatched action stacks here — split into Timeline / Seller / Buyer lenses, filtered by kind.",
  },
];

/* Manager deal-room tour — opens the same deal in MGR mode and
   walks the forecast read + coaching prep + Coaching moves tab. */
const MGR_DEAL_ROOM_STEPS = [
  {
    id: "forecast-read",
    target: '[data-tour="forecast-read"]',
    title: "Forecast read first",
    body: "Declared probability vs evidence-adjusted, the inflation in the call, and the category move recommendation — all in one read.",
  },
  {
    id: "defense-context-mgr",
    target: '[data-tour="defense"]',
    title: "Deal evidence panel",
    body: "Buyer reality lives in the same card the AE sees — same proof, same gap. Managers inspect the same truth instead of opening a separate forecast view.",
  },
  {
    id: "defense-categories-mgr",
    target: '[data-tour="defense-categories"]',
    title: "Gap + action, tagged",
    body: "Two chips frame every deal: the buyer-side gap (champion drift, missing stakeholder, process blocker, value proof, timing risk) and the recommended action (re-engage, map people, unblock, prove value, close or expand). Managers read the same taxonomy reps act on — no separate forecast vocabulary.",
  },
  {
    id: "coaching-prompt",
    target: '[data-tour="coaching-prompt"]',
    title: "Coaching prompt for the rep",
    body: "The pre-built challenge for the AE — what to ask, why, and the buyer-proof gap behind it. Coach the rep, not the buyer.",
  },
  {
    id: "next-move-tab-mgr",
    target: '[data-tour="next-move-tab"]',
    title: "Coaching moves tab",
    body: "Opens the manager's action surface: send the challenge to Slack, schedule the 1:1 question, or escalate. Every move is tied back to forecast firmness.",
  },
  {
    id: "more-tab-mgr",
    target: '[data-tour="more-tab"]',
    title: "More tabs live behind the menu",
    body: "Notes, Edit deal, Activity, Stakeholders, Meetings, Resources — the support surfaces. Same menu for AE and manager.",
    awaitNext: true,
    hint: "Next opens the menu.",
  },
  {
    id: "more-activity-mgr",
    target: '[data-tour="more-activity"]',
    title: "Example: Activity",
    body: "Pick Activity to see everything the deal has logged — buyer replies, seller moves, meetings, notes, Nudge-dispatched actions. Useful before forecast review.",
    awaitNext: true,
    hint: "Next opens Activity.",
  },
  {
    id: "activity-tab-mgr",
    target: '[data-tour="activity-tab"]',
    title: "Activity timeline",
    body: "Timeline / Seller / Buyer lenses, kind filters, and Nudge-provenance badges so the manager can read motion at a glance before the 1:1.",
  },
];

const DEALS_PAGE_STEPS = [
  {
    id: "deals-page",
    target: '[data-tour="deals-page"]',
    title: "Deal Truth board",
    body: "This page shows every deal through buyer proof, seller activity, and forecast risk. Open a card when you want the full Deal Truth room.",
  },
  {
    id: "deals-controls",
    target: '[data-tour="deals-controls"]',
    title: "Control the lens",
    body: "Use the view controls to switch between the full truth, seller activity, buyer progression, sorting, and filters.",
  },
  {
    id: "deals-pipeline",
    target: '[data-tour="deals-pipeline"]',
    title: "Stage-by-stage proof",
    body: "The board groups deals by stage while keeping buyer proof visible, so risk is not hidden inside a forecast number.",
  },
  {
    id: "deals-first-card",
    target: '[data-tour="deals-first-card"]',
    title: "The riskiest deal leads",
    body: "Cards are sorted by expected impact first, not alphabetical order. The top card is the deal most likely to change forecast quality if the next proof move lands.",
  },
  {
    id: "deals-card-proof",
    target: '[data-tour="deals-card-proof"]',
    title: "Read the buyer proof",
    body: "Inside the card, Deal Truth compares the seller story with buyer progression, then keeps the gap/action tags visible so the next review starts from evidence.",
  },
];

const MGR_DEALS_PAGE_STEPS = [
  {
    id: "mgr-deals-page",
    target: '[data-tour="deals-page"]',
    title: "Team deal review",
    body: "The manager sees the team's deals through the same buyer-proof board. It is a forecast review surface, not a CRM activity list.",
  },
  {
    id: "mgr-deals-controls",
    target: '[data-tour="deals-controls"]',
    title: "Change the inspection lens",
    body: "Switch from Deal Truth to seller or buyer views, then filter by status, gap, action category, or closing window before a forecast review.",
  },
  {
    id: "mgr-deals-pipeline",
    target: '[data-tour="deals-pipeline"]',
    title: "See risk by stage",
    body: "Stages show where team pipeline sits, but every card still exposes buyer proof so stage confidence cannot hide behind seller activity.",
  },
  {
    id: "mgr-deals-first-card",
    target: '[data-tour="deals-first-card"]',
    title: "Start with the coaching deal",
    body: "The first card is the highest-impact coaching candidate. It tells the manager where to challenge the rep before accepting the forecast call.",
  },
  {
    id: "mgr-deals-card-proof",
    target: '[data-tour="deals-card-proof"]',
    title: "Coach from the evidence",
    body: "The card shows buyer-vs-seller proof plus the gap/action tags. The manager can ask for the missing buyer-owned evidence instead of inspecting activity.",
  },
];

const ANALYTICS_PAGE_STEPS = [
  {
    id: "analytics-page",
    target: '[data-tour="analytics-page"]',
    title: "Analytics",
    body: "This page turns buyer movement, stage concentration, and won/lost patterns into a manager-readable briefing.",
  },
  {
    id: "analytics-funnel",
    target: '[data-tour="analytics-funnel"]',
    title: "Where buyers drop off",
    body: "The funnel stacks active pipeline with closed-lost dollars so you can see both current volume and the stages where buyers stopped moving.",
  },
  {
    id: "analytics-stage-focus",
    target: '[data-tour="analytics-stage-focus"]',
    title: "Open a stage",
    body: "Click the concentrated drop-off stage to inspect the live deals and the deals that were lost there.",
    awaitNext: true,
    hint: "Next opens the stage drawer.",
  },
  {
    id: "analytics-stage-drawer",
    target: '[data-tour="analytics-stage-drawer"]',
    title: "Inspect the stage",
    body: "The drawer separates active deals from closed-lost examples, then shows the specific buyer signal or gap behind the selected deal.",
  },
  {
    id: "analytics-won-loss",
    target: '[data-tour="analytics-won-loss"]',
    title: "Turn patterns into coaching",
    body: "Won/Loss patterns translate closed-deal evidence into repeatable coaching: what to avoid when losses cluster, and what to replicate when wins share a pattern.",
  },
];

const MGR_ANALYTICS_PAGE_STEPS = [
  {
    id: "mgr-analytics-page",
    target: '[data-tour="analytics-page"]',
    title: "Manager analytics",
    body: "This briefing rolls buyer drop-off, stage concentration, and closed-deal patterns into a team coaching view.",
  },
  {
    id: "mgr-analytics-funnel",
    target: '[data-tour="analytics-funnel"]',
    title: "Team drop-off by stage",
    body: "The funnel shows where team dollars are still moving and where buyers stopped. It is built for the manager's forecast inspection.",
  },
  {
    id: "mgr-analytics-stage-focus",
    target: '[data-tour="analytics-stage-focus"]',
    title: "Drill into the stage",
    body: "Open the stage with concentrated loss dollars to see which deals need review and what kind of buyer proof was missing.",
    awaitNext: true,
    hint: "Next opens the stage drawer.",
  },
  {
    id: "mgr-analytics-stage-drawer",
    target: '[data-tour="analytics-stage-drawer"]',
    title: "Use deals as examples",
    body: "The drawer gives the manager concrete deal examples for a forecast review or 1:1: active pipeline, lost deals, stage, value, and buyer signal.",
  },
  {
    id: "mgr-analytics-won-loss",
    target: '[data-tour="analytics-won-loss"]',
    title: "Coach the pattern",
    body: "Won/Loss turns the team's closed-deal history into coaching themes, so managers can repeat winning proof patterns and correct recurring loss patterns.",
  },
];

const ALERTS_PAGE_STEPS = [
  {
    id: "alerts-page",
    target: '[data-tour="alerts-page"]',
    title: "Nudges queue",
    body: "Scout, Drafter, and Forecaster rank the actions that matter now, with evidence attached to each recommendation.",
  },
  {
    id: "alerts-first",
    target: '[data-tour="alerts-first"]',
    title: "Each nudge has proof",
    body: "Open the deal, inspect the source evidence, or execute the recommended action from the queue.",
  },
];

const SETUP_PAGE_STEPS = [
  {
    id: "setup-page",
    target: '[data-tour="setup-page"]',
    title: "Setup flow",
    body: "Setup verifies the user, connects the sales stack, and generates the first buyer-proof readout.",
  },
  {
    id: "setup-card",
    target: '[data-tour="setup-card"]',
    title: "Step-by-step onboarding",
    body: "The setup card keeps the user in one guided flow instead of spreading configuration across hidden settings.",
  },
];

const PLAYBOOK_PAGE_STEPS = [
  {
    id: "playbooks-page",
    target: '[data-tour="playbooks-page"]',
    title: "Playbooks",
    body: "Playbooks are repeatable Addy sequences for common deal risks, from cold champions to legal blockers.",
  },
  {
    id: "playbook-first",
    target: '[data-tour="playbook-first"]',
    title: "Each play is measurable",
    body: "Usage and win-back rate show whether a play is creating actual movement, not just more activity.",
  },
];

function tourStepsFor(mode) {
  if (mode === "manager") return MGR_TOUR_STEPS;
  if (mode === "ae-deal-room")  return AE_DEAL_ROOM_STEPS;
  if (mode === "mgr-deal-room") return MGR_DEAL_ROOM_STEPS;
  if (mode === "deals") return DEALS_PAGE_STEPS;
  if (mode === "mgr-deals") return MGR_DEALS_PAGE_STEPS;
  if (mode === "analytics") return ANALYTICS_PAGE_STEPS;
  if (mode === "mgr-analytics") return MGR_ANALYTICS_PAGE_STEPS;
  if (mode === "alerts") return ALERTS_PAGE_STEPS;
  if (mode === "setup") return SETUP_PAGE_STEPS;
  if (mode === "playbook") return PLAYBOOK_PAGE_STEPS;
  return AE_TOUR_STEPS;
}

function clickTourTarget(selector) {
  const hit = findVisibleRect(selector);
  if (!hit || !hit.el) return false;
  hit.el.click();
  return true;
}

function findVisibleRect(selector) {
  if (!selector) return null;
  const el = document.querySelector(selector);
  if (!el) return null;
  const r = el.getBoundingClientRect();
  if (r.width === 0 && r.height === 0) return null;
  return { el, rect: r };
}

function positionTooltip(rect, tooltipEl) {
  if (!rect) return null;
  const margin = 12;
  const tipW = (tooltipEl && tooltipEl.offsetWidth) || 320;
  const tipH = (tooltipEl && tooltipEl.offsetHeight) || 160;
  const vw = window.innerWidth;
  const vh = window.innerHeight;

  const spaceBelow = vh - rect.bottom;
  const spaceAbove = rect.top;
  const placeBelow = spaceBelow >= tipH + margin || spaceBelow > spaceAbove;

  let top = placeBelow ? rect.bottom + margin : rect.top - tipH - margin;
  let left = rect.left + rect.width / 2 - tipW / 2;
  left = Math.max(margin, Math.min(vw - tipW - margin, left));
  top = Math.max(margin, Math.min(vh - tipH - margin, top));

  return { top, left, placement: placeBelow ? "below" : "above" };
}

const FIRST_CONNECTION_TOUR_KEY = "nudge.demoTour.pageSeen.v3.ae";

function DemoTour({ onNavigate, autoStartOnceKey = FIRST_CONNECTION_TOUR_KEY, autoStartMode = "ae" }) {
  const [active, setActive] = useState(false);
  const [stepIdx, setStepIdx] = useState(0);
  const [mode, setMode] = useState("ae");
  const [rect, setRect] = useState(null);
  const tooltipRef = useRef(null);
  const lastAutoStartKeyRef = useRef("");
  const steps = tourStepsFor(mode);

  // Expose start globally so a button anywhere can trigger the tour.
  useEffect(() => {
    window.startNudgeTour = (nextMode = "ae") => {
      const resolvedMode = typeof nextMode === "string"
        ? nextMode
        : nextMode && nextMode.mode;
      const nextTourMode =
        resolvedMode === "manager"        ? "manager"        :
        resolvedMode === "ae-deal-room"   ? "ae-deal-room"   :
        resolvedMode === "mgr-deal-room"  ? "mgr-deal-room"  :
        resolvedMode === "deals"          ? "deals"          :
        resolvedMode === "mgr-deals"      ? "mgr-deals"      :
        resolvedMode === "analytics"      ? "analytics"      :
        resolvedMode === "mgr-analytics"  ? "mgr-analytics"  :
        resolvedMode === "alerts"         ? "alerts"         :
        resolvedMode === "setup"          ? "setup"          :
        resolvedMode === "playbook"       ? "playbook"       :
        "ae";
      if (typeof onNavigate === "function") onNavigate(nextTourMode);
      setMode(nextTourMode);
      setStepIdx(0);
      setActive(true);
    };
    return () => { delete window.startNudgeTour; };
  }, [onNavigate]);

  // First navigation per surface: start the relevant tour once per browser.
  // The manual trigger remains available after the automatic run.
  useEffect(() => {
    if (!autoStartOnceKey || !autoStartMode) return;
    if (lastAutoStartKeyRef.current === autoStartOnceKey) return;
    lastAutoStartKeyRef.current = autoStartOnceKey;
    let alreadySeen = false;
    try {
      alreadySeen = window.localStorage?.getItem(autoStartOnceKey) === "1";
    } catch (_) {}
    if (alreadySeen) return;

    const timer = window.setTimeout(() => {
      if (typeof window.startNudgeTour !== "function") return;
      try { window.localStorage?.setItem(autoStartOnceKey, "1"); } catch (_) {}
      window.startNudgeTour(autoStartMode);
    }, 350);
    return () => window.clearTimeout(timer);
  }, [autoStartMode, autoStartOnceKey]);

  const advance = useCallback(() => {
    const cur = steps[stepIdx];
    if (!cur) return;
    if (cur.awaitNext) {
      const clicked = clickTourTarget(cur.target);
      if (clicked) return;
    }
    setStepIdx((s) => Math.min(steps.length - 1, s + 1));
  }, [stepIdx, steps]);

  // If the user jumps ahead or lands on a step whose target depends on a
  // previous interaction, perform the missing interaction automatically.
  useEffect(() => {
    if (!active) return;
    const cur = steps[stepIdx];
    if (!cur) return;
    if (findVisibleRect(cur.target)) return;

    let t = null;
    if (cur.id === "priority-detail" || cur.id === "priority-cta") {
      t = window.setTimeout(() => {
        if (!findVisibleRect(cur.target)) clickTourTarget('[data-tour="priority-select"]');
      }, 120);
    }
    if (cur.id === "action-preview" || cur.id === "action-impact") {
      t = window.setTimeout(() => {
        if (!findVisibleRect(cur.target)) clickTourTarget('[data-tour="priority-cta"]');
      }, 120);
    }
    // Analytics: if the stage drawer is not mounted yet, open the
    // highlighted stage first.
    if (cur.id === "analytics-stage-drawer" || cur.id === "mgr-analytics-stage-drawer") {
      t = window.setTimeout(() => {
        if (!findVisibleRect(cur.target)) clickTourTarget('[data-tour="analytics-stage-focus"]');
      }, 120);
    }
    // Deal-room: if the Activity menu item is missing, the More menu
    // hasn't been opened yet — click the trigger to surface it.
    if (cur.id === "more-activity" || cur.id === "more-activity-mgr") {
      t = window.setTimeout(() => {
        if (!findVisibleRect(cur.target)) clickTourTarget('[data-tour="more-tab"]');
      }, 120);
    }
    // Deal-room: if the rendered Activity tab body is missing, the user
    // hasn't selected Activity yet — click the menu item (re-opening the
    // menu first if needed).
    if (cur.id === "activity-tab" || cur.id === "activity-tab-mgr") {
      t = window.setTimeout(() => {
        if (findVisibleRect(cur.target)) return;
        if (!findVisibleRect('[data-tour="more-activity"]')) {
          clickTourTarget('[data-tour="more-tab"]');
          window.setTimeout(() => clickTourTarget('[data-tour="more-activity"]'), 120);
        } else {
          clickTourTarget('[data-tour="more-activity"]');
        }
      }, 120);
    }
    return () => { if (t) window.clearTimeout(t); };
  }, [active, stepIdx, steps]);

  // Track the current step's target rect (poll so DOM changes — modal
  // open, tab switch, scroll — pick up).
  useEffect(() => {
    if (!active) return;
    const cur = steps[stepIdx];
    if (!cur) return;

    let scrolled = false;
    const update = () => {
      const hit = findVisibleRect(cur.target);
      if (!hit) { setRect(null); return; }
      if (!scrolled) {
        scrolled = true;
        const r = hit.rect;
        const inView = r.top >= 0 && r.bottom <= window.innerHeight;
        if (!inView) {
          try { hit.el.scrollIntoView({ behavior: "smooth", block: "center" }); } catch (_) {}
        }
      }
      setRect(hit.rect);
    };
    update();
    const interval = setInterval(update, 100);
    window.addEventListener("resize", update);
    window.addEventListener("scroll", update, true);
    return () => {
      clearInterval(interval);
      window.removeEventListener("resize", update);
      window.removeEventListener("scroll", update, true);
    };
  }, [active, stepIdx, steps]);

  // Auto-advance via click listener. When the user clicks the spotlit
  // target on an awaitNext step, advance after a short delay so the new UI
  // (modal, tab content) has time to mount before the next spotlight lands.
  useEffect(() => {
    if (!active) return;
    const cur = steps[stepIdx];
    if (!cur || !cur.awaitNext) return;
    const handler = (e) => {
      const el = document.querySelector(cur.target);
      if (!el) return;
      if (!el.contains(e.target) && e.target !== el) return;
      // Use the captured stepIdx as the guard so a stale handler can't
      // advance past where the user already is.
      const captured = stepIdx;
      window.setTimeout(() => {
        setStepIdx((s) => (s === captured ? s + 1 : s));
      }, 220);
    };
    document.addEventListener("click", handler, true);
    return () => document.removeEventListener("click", handler, true);
  }, [active, stepIdx, steps]);

  // Esc / arrow keys.
  useEffect(() => {
    if (!active) return;
    const onKey = (e) => {
      if (e.key === "Escape") setActive(false);
      if (e.key === "ArrowRight" && stepIdx < steps.length - 1) advance();
      if (e.key === "ArrowLeft"  && stepIdx > 0) setStepIdx((s) => s - 1);
    };
    document.addEventListener("keydown", onKey);
    return () => document.removeEventListener("keydown", onKey);
  }, [active, stepIdx, steps.length, advance]);

  if (!active) return null;
  const cur = steps[stepIdx];
  if (!cur) return null;

  const last = stepIdx === steps.length - 1;
  const tipPos = rect ? positionTooltip(rect, tooltipRef.current) : null;

  return (
    <div className="tour-root" aria-live="polite">
      {rect && (
        <div
          className="tour-spotlight"
          style={{
            top: rect.top - 6,
            left: rect.left - 6,
            width: rect.width + 12,
            height: rect.height + 12,
          }}
          aria-hidden="true"
        />
      )}
      <div
        ref={tooltipRef}
        className={`tour-tooltip${rect ? "" : " tour-tooltip--floating"}${tipPos ? ` placement-${tipPos.placement}` : ""}`}
        role="dialog"
        aria-label={cur.title}
        style={tipPos ? { top: tipPos.top, left: tipPos.left } : undefined}
      >
        <div className="tour-step-h">
          <span className="tour-step-num">{String(stepIdx + 1).padStart(2, "0")} / {String(steps.length).padStart(2, "0")}</span>
          <button type="button" className="tour-close" onClick={() => setActive(false)} aria-label="End tour">
            <Icon name="close" size={12} />
          </button>
        </div>
        <h4 className="tour-title">{cur.title}</h4>
        <p className="tour-body">{cur.body}</p>
        {!rect && cur.hint && (
          <p className="tour-hint"><Icon name="flag" size={11} /> {cur.hint}</p>
        )}
        <div className="tour-actions">
          <button
            type="button"
            className="tour-btn tour-btn-ghost"
            onClick={() => setStepIdx((s) => Math.max(0, s - 1))}
            disabled={stepIdx === 0}
          >
            Back
          </button>
          <div className="tour-dots" aria-hidden="true">
            {steps.map((_, i) => (
              <span key={i} className={`tour-dot${i === stepIdx ? " is-on" : ""}`} />
            ))}
          </div>
          {last ? (
            <button type="button" className="tour-btn tour-btn-primary" onClick={() => setActive(false)}>
              Done
            </button>
          ) : (
            <button type="button" className="tour-btn tour-btn-primary" onClick={advance}>
              Next <Icon name="arrow" size={11} />
            </button>
          )}
        </div>
      </div>
    </div>
  );
}

/* Header button that starts the guided tour for the current page. */
function TourTrigger({ defaultMode = "ae", className = "" }) {
  const start = () => {
    if (typeof window.startNudgeTour === "function") window.startNudgeTour(defaultMode);
  };
  return (
    <div className={`tour-trigger-wrap ${className}`.trim()}>
      <button
        type="button"
        className="tour-trigger"
        onClick={start}
        title="Start guided tour"
      >
        <Icon name="sparkles" size={11} />
        <span>Guided tour</span>
      </button>
    </div>
  );
}

window.DemoTour = DemoTour;
window.TourTrigger = TourTrigger;
