// onboarding.jsx — EZText "Get started" flow
// Steps: business (basics + number details) → payment → confirmation/scheduling.
// Requires React loaded first. Styles in mila.css + onboarding.css.

const OnboardingApp = (() => {
  const { useState, useEffect, useRef } = React;

  // ── Validation + formatting helpers ─────────────────────────
  const digits = (v) => v.replace(/\D/g, "");
  const emailOk = (v) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v.trim());
  const siteOk = (v) => /\S+\.\S+/.test(v.trim());
  const zipOk = (v) => /^\d{5}$/.test(v);
  const phoneOk = (v) => digits(v).length === 10;

  function fmtPhone(v) {
    const d = digits(v).slice(0, 10);
    if (d.length <= 3) return d;
    if (d.length <= 6) return "(" + d.slice(0, 3) + ") " + d.slice(3);
    return "(" + d.slice(0, 3) + ") " + d.slice(3, 6) + "-" + d.slice(6);
  }
  function fmtCard(v) {
    const d = digits(v).slice(0, 16);
    const parts = [];
    for (let i = 0; i < d.length; i += 4) parts.push(d.slice(i, i + 4));
    return parts.join(" ");
  }
  function fmtExp(v) {
    const d = digits(v).slice(0, 4);
    if (d.length <= 2) return d;
    return d.slice(0, 2) + " / " + d.slice(2);
  }

  // ── ET scheduling helpers ───────────────────────────────────
  // Next 5 business days (Mon–Fri), hourly slots 2:00–6:00 PM ET.
  function buildDays() {
    const now = new Date();
    const nyNow = new Date(now.toLocaleString("en-US", { timeZone: "America/New_York" }));
    const days = [];
    let i = 1;
    while (days.length < 5 && i < 14) {
      const d = new Date(nyNow.getFullYear(), nyNow.getMonth(), nyNow.getDate() + i);
      const dow = d.getDay();
      if (dow !== 0 && dow !== 6) {
        const slots = [];
        for (let h = 14; h <= 18; h++) slots.push({ h });
        days.push({ date: d, slots });
      }
      i++;
    }
    return days;
  }
  const fmtHour = (h) => (h === 12 ? "12:00 PM" : h < 12 ? h + ":00 AM" : (h - 12) + ":00 PM");
  const fmtDay = (d) => d.toLocaleDateString("en-US", { weekday: "short", month: "short", day: "numeric" });

  // ── Small glyphs ────────────────────────────────────────────
  const LblCheck = () => (
    <svg className="check" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round">
      <path d="M20 6L9 17l-5-5"></path>
    </svg>
  );
  const GreenCheck = ({ size = 14 }) => (
    <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round" style={{ color: "var(--bull-500)", flexShrink: 0 }}>
      <path d="M20 6L9 17l-5-5"></path>
    </svg>
  );
  const NoIncl = () => (
    <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.6" strokeLinecap="round" strokeLinejoin="round" style={{ color: "var(--fg-subtle)", flexShrink: 0 }}>
      <path d="M18 6 6 18M6 6l12 12"></path>
    </svg>
  );
  const LockGlyph = () => (
    <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round">
      <rect x="3" y="11" width="18" height="11" rx="2"></rect>
      <path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
    </svg>
  );

  // Inline "?" help marker with hover/focus tooltip explaining why we ask.
  function HelpTip({ text, children }) {
    return (
      <span className="help-tip" tabIndex="0" role="note" aria-label={text}>
        ?
        <span className="help-bubble">{children || text}</span>
      </span>
    );
  }

  // ── Progress bar (appears once you reach payment) ────────────
  // flow + idx are computed by App so the call route can swap in "Scheduling".
  // Completed segments fill fully; the current segment fills proportionally (fill 0–1).
  // Earlier segments are clickable to navigate back.
  function ProgressBar({ flow, idx, fill, prevFills = [], visible, onNav }) {
    if (!visible) return null;
    return (
      <div className="ob-progress pop-in" aria-label="Onboarding progress">
        {flow.map((label, i) => {
          let scale = 0;
          if (i < idx) {
            // Previous segments default to fully filled, unless the parent
            // says this segment was actually only partially completed (the
            // visitor skipped ahead with the 'Pricing' shortcut).
            const pf = prevFills[i];
            scale = pf == null ? 1 : Math.min(1, Math.max(0, pf));
          } else if (i === idx) {
            scale = Math.min(1, Math.max(0.06, fill || 0));
          }
          const done = scale >= 1 && i < idx;
          const now = i === idx || (i < idx && scale < 1);
          // From the first step the 'Pricing' segment is a preview shortcut —
          // let it be clickable too so visitors can peek before filling fields.
          const canNav = !!onNav && (i < idx || (idx === 0 && i === 1));
          return (
            <button key={label} type="button"
              className={"ob-seg" + (done ? " done" : "") + (now ? " now" : "") + (canNav ? " clickable" : "")}
              onClick={canNav ? () => onNav(i) : undefined} aria-disabled={!canNav}>
              <span className="ob-seg-bar"><span className="ob-seg-fill" style={{ transform: "scaleX(" + scale + ")" }}></span></span>
              <span className="ob-seg-label">{label}</span>
            </button>
          );
        })}
      </div>
    );
  }

  function Field({ label, filled, full, hint, help, error, children }) {
    return (
      <div className={"form-field" + (filled ? " filled" : "") + (full ? " full" : "") + (error ? " error" : "")}>
        <label className="lbl">{label}{help ? <HelpTip text={help} /> : null} <LblCheck /></label>
        {children}
        {hint ? <span className="hint">{hint}</span> : null}
      </div>
    );
  }

  // ── Step 1: business basics + number details (combined) ──────
  function StepBasics({ data, set, onNext, onProgress }) {
    const [attempted, setAttempted] = useState(false);
    const [skipReveal, setSkipReveal] = useState(false);
    const hoverTimer = useRef(null);
    const okName = data.name.trim().length >= 2;
    const okSite = siteOk(data.website);
    const okEmail = emailOk(data.email);
    const checks = [okName, okSite, okEmail];
    const valid = checks.every(Boolean);
    const filledCount = checks.filter(Boolean).length;
    // Report fill ratio up so the progress bar's first segment fills as the
    // user works through the fields.
    useEffect(() => {
      if (onProgress) onProgress(filledCount / checks.length);
    }, [filledCount]);
    const next = () => { if (valid) onNext(); else setAttempted(true); };
    const err = (ok) => attempted && !ok;
    // Hovering the Next button for 250ms morphs it into a "Skip to Pricing"
    // shortcut. Click still advances (which lands on the pricing step).
    const armSkip = () => {
      clearTimeout(hoverTimer.current);
      hoverTimer.current = setTimeout(() => setSkipReveal(true), 250);
    };
    const disarmSkip = () => {
      clearTimeout(hoverTimer.current);
      setSkipReveal(false);
    };
    useEffect(() => () => clearTimeout(hoverTimer.current), []);
    return (
      <div className="demo-card phase phase-enter" data-screen-label="Onboarding — business basics">
        <div className="ob-card-head">
          <h3 className="ob-title">Tell us about your business</h3>
        </div>
        <div className="form-grid">
          <Field label="Business name" filled={okName} error={err(okName)}>
            <input className="field" value={data.name} onChange={(e) => set("name", e.target.value)} placeholder="Your business name" />
          </Field>
          <Field label="Business website" filled={okSite} error={err(okSite)}
            help="Your website is the starting point for the agent to understand your business and to train itself on typical Q&A.">
            <input className="field" type="url" value={data.website} onChange={(e) => set("website", e.target.value)} placeholder="yourwebsite.com" />
          </Field>
          <Field label="Email address" filled={okEmail} full error={err(okEmail)}
            help="We'll send setup updates and your agent's activity here.">
            <input className="field" type="email" value={data.email} onChange={(e) => set("email", e.target.value)} placeholder="you@yourbusiness.com" />
          </Field>
        </div>
        <div className="ob-actions">
          <span className={"ob-error-msg" + (attempted && !valid ? " show" : "")}>
            Please complete all fields to continue.
          </span>
          <button
            className={"btn btn-lg ob-next" + (skipReveal ? " ob-next-skip" : " btn-primary")}
            onClick={next}
            disabled={!valid}
            onMouseEnter={armSkip}
            onMouseLeave={disarmSkip}
            onFocus={armSkip}
            onBlur={disarmSkip}>
            {skipReveal ? (
              <span className="ob-next-skip-stack">
                <span className="ob-next-skip-small">Skip to</span>
                <span className="ob-next-skip-big">Pricing</span>
              </span>
            ) : "Next →"}
          </button>
        </div>
      </div>
    );
  }

  // ── Step 2: payment (subscribe now · or talk to the team) ────
  function StepPayment({ data, set, onDone, onProgress, basicsComplete, onBack }) {
    const [mode, setMode] = useState("pay"); // "pay" | "call"
    const [card, setCard] = useState("");
    const [exp, setExp] = useState("");
    const [cvc, setCvc] = useState("");
    const [holder, setHolder] = useState("");
    const [billZip, setBillZip] = useState(data.zip || "");
    const [paying, setPaying] = useState(false);
    const [payMethod, setPayMethod] = useState("card"); // "card" | "applepay"
    const [plan, setPlan] = useState("full"); // "full" $200 | "lite" $100
    const [attempted, setAttempted] = useState(false);
    // Promo code state. When applied, firstMonthPrice = price * (1 - %/100).
    const [promo, setPromo] = useState(null); // { code, label, percentOff } | null
    const [promoInput, setPromoInput] = useState("");
    const [promoChecking, setPromoChecking] = useState(false);
    const [promoError, setPromoError] = useState("");
    const [promoSuccess, setPromoSuccess] = useState("");  // green "we appreciate" copy
    const [promoFailCount, setPromoFailCount] = useState(0);
    const [priceBurst, setPriceBurst] = useState(false);
    const price = plan === "lite" ? 100 : 200;
    const firstMonthPrice = promo
      ? Math.max(0, Math.round(price * (1 - promo.percentOff / 100)))
      : price;
    const checks = [
      digits(card).length === 16,
      digits(exp).length === 4,
      digits(cvc).length >= 3,
      holder.trim().length >= 2,
      zipOk(billZip),
    ];
    const cardValid = checks.every(Boolean);
    const emailValid = emailOk(data.email);
    // Card requires every field. Apple Pay hands off to the wallet, so the
    // in-app validity is just emailValid (gated separately below).
    const valid = payMethod === "card" ? cardValid : true;
    const filledCount = checks.filter(Boolean).length;
    // Pricing-segment progress: a small baseline on arrival ("Subscribe and
    // go live"), more when they switch to "Speak with our team first", and
    // it tops up as they fill the card or confirm an email for the call.
    useEffect(() => {
      var f;
      if (mode === "pay") {
        const cardFrac = filledCount / checks.length;
        f = 0.25 + 0.6 * cardFrac;
      } else {
        f = 0.55 + (emailValid ? 0.25 : 0);
      }
      if (onProgress) onProgress(f);
    }, [filledCount, mode, payMethod, emailValid]);
    // Inline Stripe Elements card form. Mounts a single combined CardElement
    // into #card-host when the Card tab is selected. On Subscribe we
    // createPaymentMethod here and POST it to /api/login?op=subscribe; the
    // server creates the Customer + Subscription with the same coupon math
    // as the hosted-checkout flow. 3DS / SCA challenges are handled via
    // confirmCardPayment using the returned clientSecret.
    const cardHostRef = useRef(null);
    const stripeRef = useRef(null);
    const cardElementRef = useRef(null);
    const [cardReady, setCardReady] = useState(false);
    const [cardError, setCardError] = useState("");

    useEffect(() => {
      if (payMethod !== "card" || mode !== "pay") return;
      if (!window.Stripe) {
        setCardError("Payments aren't loaded yet — refresh the page.");
        return;
      }
      let cancelled = false;
      let card = null;
      // Pull the publishable key from the server so live↔test swaps only
      // touch Vercel env vars. Falls back to the inline window value if
      // the endpoint isn't deployed yet.
      (async () => {
        let pk = "";
        try {
          const r = await fetch("/api/login?op=stripe-pub");
          const j = await r.json();
          if (j && j.publishableKey) pk = j.publishableKey;
        } catch (_) {}
        if (!pk) pk = window.STRIPE_PUBLISHABLE_KEY || "";
        if (cancelled) return;
        if (!pk) {
          setCardError("Payments aren't configured yet.");
          return;
        }
        const stripe = window.Stripe(pk);
        stripeRef.current = stripe;
        const elements = stripe.elements();
        card = elements.create("card", {
          hidePostalCode: false,
          style: {
            base: {
              fontSize: "15px",
              color: "#101010",
              fontFamily: "Inter, -apple-system, system-ui, sans-serif",
              "::placeholder": { color: "#8a8a8a" },
            },
            invalid: { color: "#b91c1c" },
          },
        });
        const host = cardHostRef.current;
        if (host) {
          host.innerHTML = "";
          card.mount(host);
          cardElementRef.current = card;
          card.on("ready", () => setCardReady(true));
          card.on("change", (e) => setCardError(e.error?.message || ""));
        }
      })();
      return () => {
        cancelled = true;
        try { card && card.destroy(); } catch (_) {}
        cardElementRef.current = null;
        setCardReady(false);
      };
    }, [payMethod, mode]);

    // Kick the visitor to Stripe Checkout. The page handles all card / Apple
    // Pay / Google Pay / Link entry; we just hand off plan + email + promo.
    // On success Stripe sends them back to /get-started?paid=1 (handled by
    // the App shell below), which lights up the celebration screen and pings
    // the operator email.
    // Apple Pay tab: redirect to Stripe-hosted Checkout (Apple Pay button
    // appears on supported devices). payment_method_types:['card'] on the
    // server suppresses Link so the email field doesn't try to verify the
    // buyer under a phone-linked Link account.
    const pay = async () => {
      if (paying) return;
      setPaying(true);
      try {
        const r = await fetch("/api/login?op=checkout", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({
            plan: plan === "lite" ? "standard" : "premium",
            email: data.email,
            promoCode: promo?.code || "",
          }),
        });
        const j = await r.json().catch(() => ({}));
        if (!r.ok || !j.url) throw new Error(j.error || "Checkout couldn't start. Try again.");
        window.location.href = j.url;
      } catch (e) {
        setPaying(false);
        alert(e.message);
      }
    };

    // Card tab: confirm the subscription right here without a hosted-page
    // redirect. createPaymentMethod → POST to op=subscribe → server creates
    // Customer + Subscription with the matching coupon → run 3DS if asked
    // → land on /get-started?paid=1.
    const cardSubscribe = async () => {
      if (paying) return;
      if (!emailValid) { setAttempted(true); return; }
      const stripe = stripeRef.current;
      const card = cardElementRef.current;
      if (!stripe || !card) { setCardError("Card form isn't ready yet."); return; }
      setPaying(true);
      setCardError("");
      try {
        const pmResult = await stripe.createPaymentMethod({
          type: "card",
          card,
          billing_details: { email: data.email, name: data.name || undefined },
        });
        if (pmResult.error) {
          setCardError(pmResult.error.message);
          setPaying(false);
          return;
        }
        const r = await fetch("/api/login?op=subscribe", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({
            plan: plan === "lite" ? "standard" : "premium",
            email: data.email,
            name: data.name || "",
            paymentMethodId: pmResult.paymentMethod.id,
            promoCode: promo?.code || "",
          }),
        });
        const j = await r.json().catch(() => ({}));
        if (!r.ok || !j.ok) throw new Error(j.error || "Subscription couldn't be created.");
        // payment_behavior: 'default_incomplete' on the server leaves the
        // subscription in 'incomplete' until we confirm the PaymentIntent
        // on the client. Confirm whenever Stripe gives us a clientSecret
        // (requires_confirmation for normal card, requires_action for 3DS)
        // — only 'succeeded' or null means we can skip. Without this the
        // subscription would sit forever in Incomplete in the Stripe
        // dashboard and the invoice receipt would never send.
        if (j.clientSecret && j.paymentIntentStatus !== "succeeded") {
          const conf = await stripe.confirmCardPayment(j.clientSecret);
          if (conf.error) throw new Error(conf.error.message);
        }
        // Hand the success page everything the operator email needs.
        const successParams = new URLSearchParams({
          paid: "1",
          plan: plan === "lite" ? "standard" : "premium",
          price: String(price),
          amount: String(firstMonthPrice),
          method: "card",
        });
        if (promo?.code) successParams.set("promo", promo.code);
        window.location.href = "/get-started?" + successParams.toString();
      } catch (e) {
        setPaying(false);
        setCardError(e.message);
      }
    };
    // Internal helper — apply a verified promo and run the success animation.
    const acceptPromo = (entry, successMsg) => {
      setPromo(entry);
      setPromoInput("");
      setPromoError("");
      setPromoSuccess(successMsg || "");
      setPriceBurst(true);
      // Hold the burst class long enough for both the 1.65s price scale-up
      // animation and the 8.8s confetti to complete without yanking the canvas.
      setTimeout(() => setPriceBurst(false), 8800);
    };
    const applyPromo = async () => {
      const code = (promoInput || "").trim();
      if (!code || promoChecking) return;
      setPromoChecking(true);
      setPromoError("");
      setPromoSuccess("");
      try {
        const r = await fetch("/api/login?op=promo-verify", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ code }),
        });
        const j = await r.json().catch(() => ({}));
        if (!r.ok || !j.ok) throw new Error(j.error || "That promo code isn't recognized.");
        setPromoFailCount(0);
        acceptPromo({ code: j.code, label: j.label, percentOff: j.percentOff });
      } catch (e) {
        const nextFails = promoFailCount + 1;
        setPromoFailCount(nextFails);
        // Persistence reward: after the third miss, drop them PERSISTENCE10.
        if (nextFails >= 3) {
          acceptPromo(
            { code: "PERSISTENCE10", label: "Persistence reward", percentOff: 10 },
            "We appreciate the persistence! Here's a valid promo code for your efforts."
          );
        } else {
          setPromoError(e.message);
        }
      } finally {
        setPromoChecking(false);
      }
    };
    const removePromo = () => {
      setPromo(null);
      setPromoError("");
      setPromoSuccess("");
      setPromoFailCount(0);
    };
    // Apple Pay flow: only needs a valid email — Stripe collects everything
    // else on its hosted page. Business name / website are nice-to-have but
    // don't block the subscription.
    const trySubscribe = () => {
      if (!emailValid) { setAttempted(true); return; }
      pay();
    };
    // Talking to the team only requires an email — they can finish their
    // business details on the call. Any other ask blocks scheduling.
    const trySchedule = () => {
      if (!emailValid) { setAttempted(true); return; }
      onDone("call");
    };
    // The 'finish Business page' prompt is only for the pay flow. Call mode
    // is intentionally a low-friction path.
    const needsBasics = attempted && mode === "pay" && !basicsComplete;
    const needsEmail = attempted && mode === "call" && !emailValid;
    // Reset the attempted flag when switching modes so an earlier 'pay'
    // attempt doesn't leave the basics prompt visible in call mode.
    useEffect(() => { setAttempted(false); }, [mode]);
    const ErrorBadge = ({ children }) => (
      <div className="ob-error-badge" role="alert">
        <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10"></circle><path d="M12 8v4"></path><path d="M12 16h.01"></path></svg>
        <span>{children}</span>
      </div>
    );
    return (
      <div className="demo-card phase phase-enter" data-screen-label="Onboarding — payment">
        <div className="ob-card-head" style={{ position: "relative" }}>
          {onBack ? (
            <button type="button" className="ob-step-back" onClick={onBack} aria-label="Back to business">
              <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round"><path d="M19 12H5"></path><path d="M12 19l-7-7 7-7"></path></svg>
              Back
            </button>
          ) : null}
          <h3 className="ob-title">Start your subscription</h3>
        </div>
        <div className={"ob-pay-grid" + (mode === "call" ? " call-mode" : "")}>
          <div className="ob-pay-left">
          <div className="ob-plan">
            <div className="ob-plan-head">
              <span className="t-eyebrow">{plan === "lite" ? "Standard" : "Premium"}</span>
              <button type="button" className="ob-plan-switch" onClick={() => setPlan(plan === "lite" ? "full" : "lite")}>
                {plan === "lite" ? "Switch to Premium · $200/mo" : "Switch to Standard · $100/mo"}
              </button>
            </div>
            <div className={"ob-price" + (priceBurst ? " burst" : "")}>
              {promo ? (
                <React.Fragment>
                  <span className="ob-price-num">{"$" + firstMonthPrice}</span>
                  <span className="ob-price-per">/first month</span>
                  {priceBurst ? <Confetti durationMs={8800} /> : null}
                </React.Fragment>
              ) : (
                <React.Fragment>
                  <span className="ob-price-num">{"$" + price}</span>
                  <span className="ob-price-per">/month</span>
                </React.Fragment>
              )}
            </div>
            <ul className="ob-plan-list">
              <li><GreenCheck /> AI texting agent, trained on your business</li>
              <li><GreenCheck /> Answers every text in real time, 24/7</li>
              <li><GreenCheck /> Every conversation in one dashboard</li>
              <li><GreenCheck /> Complimentary, 1-on-1 onboarding to set you up for success</li>
              {plan === "lite" ? (
                <li className="ob-plan-off">
                  <NoIncl />
                  <span><span className="ob-strike">Dedicated local phone number</span><sup className="ob-q-sup"><HelpTip text="Without a dedicated number, customers reach a shared EZText number. Their first text just needs to name your business, or the agent will simply ask which business they're trying to reach. Upgrade to a dedicated number anytime.">
                    <span className="hb-p">Without a dedicated number, customers reach a shared EZText number. Their first text just needs to name your business — or the agent will simply ask which business they're trying to reach before it assists.</span>
                    <span className="hb-p">Upgrade to a dedicated number anytime.</span>
                  </HelpTip></sup></span>
                </li>
              ) : (
                <li><GreenCheck /> Dedicated local phone number</li>
              )}
            </ul>
            <span className="hint">
              {promo
                ? "Then $" + price + " / month. Cancel anytime."
                : "Billed monthly. Cancel anytime."}
            </span>
          </div>
          {/* Promo code row — outside the plan card, full-width below it. */}
          <div className={"ob-promo" + (promo ? " applied" : "")}>
            {promo ? (
              <div className="ob-promo-applied">
                <GreenCheck size={12} />
                <span className="ob-promo-applied-text">
                  {promo.code} applied · {promo.percentOff}% off first month
                </span>
                <button type="button" className="ob-promo-remove" onClick={removePromo} aria-label="Remove promo code">×</button>
              </div>
            ) : (
              <React.Fragment>
                <input
                  className="ob-promo-input"
                  type="text"
                  autoCorrect="off"
                  autoCapitalize="off"
                  placeholder="Promo code"
                  value={promoInput}
                  onChange={(e) => { setPromoInput(e.target.value); setPromoError(""); }}
                  onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); applyPromo(); } }}
                  disabled={promoChecking}
                />
                <button type="button" className="ob-promo-apply"
                  onClick={applyPromo}
                  disabled={!promoInput.trim() || promoChecking}>
                  {promoChecking ? "…" : "Apply"}
                </button>
              </React.Fragment>
            )}
          </div>
          {promoSuccess ? (
            <span className="ob-promo-success">{promoSuccess}</span>
          ) : promoError ? (
            <span className="ob-promo-error">{promoError}</span>
          ) : null}
          </div>
          <div className="ob-checkout">
            <div className="ob-mode" role="radiogroup" aria-label="How would you like to start?">
              <button type="button" role="radio" aria-checked={mode === "pay"}
                className={"ob-mode-opt" + (mode === "pay" ? " active" : "")} onClick={() => setMode("pay")}>
                <span className="ob-mode-dot"></span>
                <span className="ob-mode-text">
                  <span className="ob-mode-title">Subscribe and go live</span>
                  <span className="ob-mode-sub">Pay today and we'll have your agent set up within hours.</span>
                </span>
              </button>
              <button type="button" role="radio" aria-checked={mode === "call"}
                className={"ob-mode-opt" + (mode === "call" ? " active" : "")} onClick={() => setMode("call")}>
                <span className="ob-mode-dot"></span>
                <span className="ob-mode-text">
                  <span className="ob-mode-title">Speak with our team first</span>
                  <span className="ob-mode-sub">Book a call with our team so you can hit the ground running.</span>
                </span>
              </button>
            </div>

            {mode === "pay" ? (
              <React.Fragment>
                <Field label="Email" filled={emailValid}>
                  <input className="field" type="email" value={data.email}
                    onChange={(e) => set("email", e.target.value)}
                    placeholder="you@yourbusiness.com" />
                </Field>
                <div className="ob-paytabs" role="radiogroup" aria-label="Payment method">
                  <button type="button" role="radio" aria-checked={payMethod === "card"}
                    className={"ob-paytab" + (payMethod === "card" ? " active" : "")} onClick={() => setPayMethod("card")}>
                    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="2" y="5" width="20" height="14" rx="2"></rect><path d="M2 10h20"></path></svg>
                    Card
                  </button>
                  <button type="button" role="radio" aria-checked={payMethod === "applepay"}
                    className={"ob-paytab" + (payMethod === "applepay" ? " active" : "")} onClick={() => setPayMethod("applepay")}>
                    <span className="ob-applepay-wordmark">
                      <svg width="14" height="16" viewBox="0 0 17 20" fill="currentColor" aria-hidden="true"><path d="M13.79 10.7c-.02-2.18 1.79-3.23 1.87-3.28-1.02-1.49-2.61-1.7-3.18-1.72-1.35-.14-2.64.79-3.33.79-.7 0-1.75-.78-2.88-.75-1.47.02-2.85.86-3.61 2.17C.96 10.59 2.1 14.34 3.65 16.4c.76 1.01 1.66 2.14 2.84 2.1 1.14-.05 1.57-.74 2.95-.74 1.38 0 1.77.74 2.97.71 1.23-.02 2.01-1.03 2.76-2.04.87-1.16 1.23-2.29 1.25-2.35-.03-.01-2.4-.92-2.42-3.65zM11.62 4.1c.63-.76 1.05-1.82.94-2.87-.91.04-2.01.6-2.65 1.36-.58.67-1.09 1.74-.95 2.77 1.01.08 2.04-.51 2.66-1.26z"/></svg>
                      Pay
                    </span>
                  </button>
                </div>
                {payMethod === "card" ? (
                  <React.Fragment>
                    <Field label="Card details" filled={cardReady && !cardError}>
                      <div ref={cardHostRef} className="ob-card-host"></div>
                    </Field>
                    {cardError ? <span className="ob-promo-error">{cardError}</span> : null}
                    <button className="btn btn-primary btn-lg" style={{ width: "100%", justifyContent: "center" }} disabled={paying || !cardReady} onClick={cardSubscribe}>
                      {paying ? <span className="ob-spin ob-spin-light"></span> : null}
                      {paying ? "Processing\u2026" : promo ? ("Subscribe \u00b7 $" + firstMonthPrice + " today") : ("Subscribe \u00b7 $" + price + "/month")}
                    </button>
                    {attempted && !emailValid ? (
                      <ErrorBadge>Add a valid email above so we can reach you.</ErrorBadge>
                    ) : null}
                    <span className="ob-stripe-note"><LockGlyph /> Payments secured by Stripe</span>
                  </React.Fragment>
                ) : (
                  <div className="ob-paypal">
                    <p className="ob-paypal-copy">{"Continue to Stripe to confirm " + (promo ? ("$" + firstMonthPrice + " today, then $" + price + "/month from month 2") : ("the $" + price + "/month subscription")) + " with Apple Pay. Cancel anytime."}</p>
                    <button className="btn ob-applepay-btn btn-lg" disabled={paying} onClick={trySubscribe}>
                      {paying ? <span className="ob-spin ob-spin-light"></span> : null}
                      {paying ? "Connecting\u2026" : (
                        <React.Fragment>
                          Continue with&nbsp;
                          <svg width="16" height="18" viewBox="0 0 17 20" fill="currentColor" aria-hidden="true" style={{ marginRight: 2 }}><path d="M13.79 10.7c-.02-2.18 1.79-3.23 1.87-3.28-1.02-1.49-2.61-1.7-3.18-1.72-1.35-.14-2.64.79-3.33.79-.7 0-1.75-.78-2.88-.75-1.47.02-2.85.86-3.61 2.17C.96 10.59 2.1 14.34 3.65 16.4c.76 1.01 1.66 2.14 2.84 2.1 1.14-.05 1.57-.74 2.95-.74 1.38 0 1.77.74 2.97.71 1.23-.02 2.01-1.03 2.76-2.04.87-1.16 1.23-2.29 1.25-2.35-.03-.01-2.4-.92-2.42-3.65zM11.62 4.1c.63-.76 1.05-1.82.94-2.87-.91.04-2.01.6-2.65 1.36-.58.67-1.09 1.74-.95 2.77 1.01.08 2.04-.51 2.66-1.26z"/></svg>
                          Pay
                        </React.Fragment>
                      )}
                    </button>
                    {attempted && !emailValid ? (
                      <ErrorBadge>Add a valid email above so we can reach you.</ErrorBadge>
                    ) : null}
                    <span className="ob-stripe-note"><LockGlyph /> Apple Pay through Stripe</span>
                  </div>
                )}

              </React.Fragment>
            ) : (
              <div className="ob-callbox">
                <p>Prefer a guided start? Book a quick call and our team will walk you through how EZText works and answer anything before you subscribe.</p>
                <Field label="Email" filled={emailValid}>
                  <input className="field" type="email" value={data.email}
                    onChange={(e) => set("email", e.target.value)}
                    placeholder="you@yourbusiness.com" />
                </Field>
                <button className="btn btn-primary btn-lg" style={{ width: "100%", justifyContent: "center" }} onClick={trySchedule}>
                  Schedule time to talk →
                </button>
                {needsEmail ? (
                  <ErrorBadge>Add a valid email so we can reach you.</ErrorBadge>
                ) : null}
                <span className="ob-stripe-note"><LockGlyph /> No charge today — you only pay once you're ready.</span>
              </div>
            )}
          </div>
        </div>
      </div>
    );
  }

  // ── Confetti burst (canvas) ─────────────────────────────────
  // durationMs lets callers stretch the animation. Defaults to the
  // post-payment celebration timings; the pricing-card promo burst passes
  // 2× so the cascade lingers under the price longer.
  function Confetti({ durationMs = 4400 }) {
    const ref = useRef(null);
    const fadeStart = durationMs * (2800 / 4400);
    const fadeLen = durationMs - fadeStart;
    useEffect(() => {
      if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
      const canvas = ref.current;
      if (!canvas) return;
      const ctx = canvas.getContext("2d");
      let w, h, raf;
      const colors = ["#FF5A1F", "#1F9D55", "#FF7A3D", "#1B1913", "#F5A623", "#2A7DE1"];
      const resize = () => { w = canvas.width = canvas.offsetWidth; h = canvas.height = canvas.offsetHeight; };
      resize();
      window.addEventListener("resize", resize);
      const parts = [];
      for (let i = 0; i < 170; i++) {
        parts.push({
          x: Math.random() * w,
          y: -20 - Math.random() * h * 0.6,
          r: 4 + Math.random() * 6,
          c: colors[(Math.random() * colors.length) | 0],
          vx: (Math.random() - 0.5) * 1.6,
          vy: 2.2 + Math.random() * 3.6,
          rot: Math.random() * Math.PI,
          vr: (Math.random() - 0.5) * 0.26,
          sway: Math.random() * 6,
        });
      }
      const start = performance.now();
      const draw = (t) => {
        const elapsed = t - start;
        ctx.clearRect(0, 0, w, h);
        parts.forEach((p) => {
          p.x += p.vx + Math.sin(t / 600 + p.sway) * 0.6;
          p.y += p.vy;
          p.rot += p.vr;
          ctx.save();
          ctx.translate(p.x, p.y);
          ctx.rotate(p.rot);
          ctx.globalAlpha = elapsed > fadeStart ? Math.max(0, 1 - (elapsed - fadeStart) / fadeLen) : 1;
          ctx.fillStyle = p.c;
          ctx.fillRect(-p.r / 2, -p.r / 2, p.r, p.r * 0.62);
          ctx.restore();
        });
        if (elapsed < durationMs) raf = requestAnimationFrame(draw);
      };
      raf = requestAnimationFrame(draw);
      return () => { cancelAnimationFrame(raf); window.removeEventListener("resize", resize); };
    }, []);
    return <canvas ref={ref} className="ob-confetti" aria-hidden="true"></canvas>;
  }

  // ── Full-window booking celebration ─────────────────────────
  function BookedCelebration({ booked, data, isCall, onBack }) {
    return (
      <div className="ob-celebrate" role="dialog" aria-label="Booking confirmed">
        <Confetti />
        <div className="ob-celebrate-inner pop-in">
          <span className="ob-done-badge ob-done-badge-lg"><GreenCheck size={34} /></span>
          <h2 className="ob-celebrate-title">You're booked{data.name ? ", " + data.name : ""}.</h2>
          <p className="ob-celebrate-when">{fmtDay(booked.day)} · {fmtHour(booked.h)} ET</p>
          <p className="ob-celebrate-copy">
            A member of our team will reach out with a calendar invitation to <strong>{data.email}</strong>. Talk soon.
          </p>
          <button className="btn btn-ghost ob-proceed-link" onClick={onBack}>Set up payment now →</button>
        </div>
      </div>
    );
  }

  // ── Call route: request received (confetti, no slot picker) ──
  function CallRequestConfirmed({ data, onBack }) {
    const first = (data.name || "").trim().split(/\s+/)[0];
    return (
      <div className="ob-celebrate" role="dialog" aria-label="Request received">
        <Confetti />
        <div className="ob-celebrate-inner pop-in">
          <span className="ob-done-badge ob-done-badge-lg"><GreenCheck size={34} /></span>
          <h2 className="ob-celebrate-title">Thanks{first ? ", " + first : ""}!</h2>
          <p className="ob-celebrate-copy">
            We've received your request. A member of our team will reach out to schedule a time to talk — we'll be in touch shortly.
          </p>
          <div className="ob-done-actions">
            <button type="button" className="btn btn-ghost ob-proceed-link" onClick={onBack}>← Back</button>
            <a className="btn btn-ghost ob-proceed-link" href="/">Return to homepage →</a>
          </div>
        </div>
      </div>
    );
  }

  // ── Step 3: confirmation + scheduling ───────────────────────
  function StepDone({ data, mode, onBack }) {
    const [days] = useState(buildDays);
    const firstOpen = days.findIndex((d) => d.slots.length > 0);
    const [dayIdx, setDayIdx] = useState(firstOpen);
    const [slot, setSlot] = useState(null);
    const [booked, setBooked] = useState(null);
    const isCall = mode === "call";
    if (isCall) return <CallRequestConfirmed data={data} onBack={onBack} />;
    // Pay-mode confirmation: confetti, fixed message, no back button, no
    // scheduling block — they're done.
    return (
      <div className="ob-celebrate" data-screen-label="Onboarding — confirmation">
        <Confetti />
        <div className="ob-celebrate-inner pop-in">
          <span className="ob-done-badge ob-done-badge-lg"><GreenCheck size={34} /></span>
          <h2 className="ob-celebrate-title">Payment Successful.</h2>
          <p className="ob-celebrate-copy">
            Our team is securing your phone number and setting up your agent. We'll reach out shortly with next steps and our invoice.
          </p>
        </div>
      </div>
    );
  }

  // Fire-and-forget submission to the operator inbox.
  function sendContact(payload) {
    try {
      fetch("/api/contact", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(payload),
        keepalive: true,
      }).catch(() => {});
    } catch (e) {}
  }

  // ── App shell ───────────────────────────────────────────────
  function App() {
    const [step, setStep] = useState(0); // 0 business · 1 payment · 2 done
    const [mode, setMode] = useState("pay");
    const [basicsFill, setBasicsFill] = useState(0);
    const [payFill, setPayFill] = useState(0);
    const [data, setData] = useState({ name: "", website: "", email: "", phone: "", zip: "" });
    const [contactSent, setContactSent] = useState(false);
    const set = (k, v) => setData((d) => Object.assign({}, d, { [k]: v }));
    const go = (s) => { setStep(s); window.scrollTo({ top: 0, behavior: "smooth" }); };

    // Pre-fill from Biz Demo. When a prospect clicks the
    // "Get your own EZText agent →" link from /<slug>, they land
    // here with ?demo_name=... &demo_website=... — we know which
    // business they were just chatting with, so type those values
    // into the Business Name + Website fields on their behalf,
    // letter-by-letter, so it's clear the form is being filled
    // FOR them. They only need to add their email after that.
    useEffect(() => {
      const q = new URLSearchParams(window.location.search);
      const demoName = (q.get("demo_name") || "").slice(0, 80);
      const demoWebsite = (q.get("demo_website") || "").slice(0, 240);
      if (!demoName && !demoWebsite) return;
      let cancelled = false;
      let timeouts = [];
      async function typeInto(key, value) {
        if (!value) return;
        // ~22ms per character feels like fast typing without being instant.
        for (let i = 1; i <= value.length; i += 1) {
          if (cancelled) return;
          const slice = value.slice(0, i);
          set(key, slice);
          await new Promise((r) => { const t = setTimeout(r, 22); timeouts.push(t); });
        }
      }
      (async () => {
        // Small delay so the form is mounted + the user sees the
        // typing animation start, rather than the values appearing
        // already-filled mid-page-load.
        await new Promise((r) => { const t = setTimeout(r, 350); timeouts.push(t); });
        await typeInto("name", demoName);
        if (cancelled) return;
        await new Promise((r) => { const t = setTimeout(r, 200); timeouts.push(t); });
        await typeInto("website", demoWebsite);
      })();
      return () => { cancelled = true; timeouts.forEach(clearTimeout); };
    }, []);

    // Hide the 'Back to site' nav button throughout onboarding so the header
    // stays clean. The in-card 'Back' button on the Pricing page is the only
    // backwards-navigation affordance we want here. The wordmark still
    // doubles as a link home.
    useEffect(() => {
      const back = document.getElementById("back-to-site");
      if (back) back.style.display = "none";
    }, [step]);

    // Stripe sends the buyer back to this page with ?paid=1 + context
    // (plan, promo, method, amount, session_id) on a successful checkout.
    // Jump them straight to the celebration screen and notify the operator
    // at noahmill465@gmail.com with everything we know about the customer.
    // On ?canceled=1 stay on the pricing page (no action needed).
    useEffect(() => {
      const q = new URLSearchParams(window.location.search);
      if (q.get("paid") === "1") {
        setMode("pay");
        setStep(2);
        const planParam = q.get("plan") || "";
        const priceParam = q.get("price") || (planParam === "premium" ? "200" : planParam === "standard" ? "100" : "");
        const amountParam = q.get("amount") || priceParam;
        const promoParam = q.get("promo") || "";
        const methodParam = q.get("method") || "";
        sendContact({
          source: "onboarding-subscribed",
          subject: "EZTEXT SUBSCRIPTION",
          business: data.name,
          website: data.website,
          email: data.email,
          plan: planParam,
          promoCode: promoParam,
          amountToday: amountParam ? "$" + amountParam : "",
          recurring: priceParam ? "$" + priceParam + "/month" : "",
          paymentMethod: methodParam,
          sessionId: q.get("session_id") || "",
          notes: "Stripe payment succeeded — confirm + provision the agent.",
        });
        // Strip the query string so a refresh doesn't re-fire the email.
        window.history.replaceState({}, "", window.location.pathname);
      }
    }, []);

    // True when every Business-page field is valid. Pricing actions gate on
    // this so a visitor can't subscribe or schedule without filling it.
    const basicsComplete =
      data.name.trim().length >= 2 &&
      siteOk(data.website) &&
      emailOk(data.email);
    const advanceFromBasics = () => {
      if (!contactSent) {
        sendContact({
          source: "onboarding-business",
          business: data.name,
          website: data.website,
          email: data.email,
          phone: data.phone,
          zip: data.zip,
        });
        setContactSent(true);
      }
      go(1);
    };
    const finishStep1 = (m) => {
      // Re-send when they pick "call me" so the operator knows the path taken.
      sendContact({
        source: m === "call" ? "onboarding-call" : "onboarding-payment-intent",
        business: data.name,
        website: data.website,
        email: data.email,
        phone: data.phone,
        zip: data.zip,
        notes: m === "call" ? "Picked 'talk to the team' route" : "Submitted card details (front-end only)",
      });
      setMode(m);
      go(2);
    };

    // Call route swaps "Pricing" for "Scheduling" once you're on the scheduling page.
    const isCallDone = step === 2 && mode === "call";
    const flow = isCallDone
      ? ["Business", "Scheduling", "Onboarding"]
      : ["Business", "Pricing", "Onboarding"];
    const idx = isCallDone ? 1 : step;
    let fill = 0;
    if (step === 0) fill = basicsFill;
    else if (step === 1) fill = payFill;
    else fill = mode === "pay" ? 1 : 0.5;
    // Previous segments default to fully-done. If the visitor skipped the
    // Business page by jumping ahead with 'Pricing', keep that segment at
    // its actual partial fill instead of showing it as complete.
    const prevFills = [basicsComplete ? 1 : basicsFill];
    // From step 0 the 'Pricing' label is a preview shortcut — let them peek
    // at pricing without first filling the form. The Next button still
    // requires the fields, so they can't fully advance without them.
    const onNav = (i) => {
      if (i === 0) go(0);
      else if (i === 1) go(1);
    };

    return (
      <div className={"ob-wrap" + (step === 1 ? " wide" : "")}>
        <ProgressBar flow={flow} idx={idx} fill={fill} prevFills={prevFills} visible={true} onNav={onNav} />
        {step === 0 && <StepBasics data={data} set={set} onNext={advanceFromBasics} onProgress={setBasicsFill} />}
        {step === 1 && <StepPayment data={data} set={set} onProgress={setPayFill} onDone={finishStep1} basicsComplete={basicsComplete} onBack={() => go(0)} />}
        {step === 2 && <StepDone data={data} mode={mode} onBack={() => go(1)} />}
      </div>
    );
  }

  return App;
})();

const onboardingRoot = document.getElementById("onboarding-root");
if (onboardingRoot) ReactDOM.createRoot(onboardingRoot).render(<OnboardingApp />);
