// byo.jsx - "Try the agent on your own website" demo
// Paste a real website → agent is "built" → text it like a customer would.
// The agent answers as comprehensively as it can from what it read.
// After 5 customer texts the demo caps: it thanks the user and points them at
// the "Get started" button in the header to make the agent their own.
// Persists the inferred profile per-website in localStorage so a revisit is instant.
// Requires React, ios-frame.jsx, chat-ui.jsx loaded first.

const BYO = (() => {
  const { useState, useEffect, useRef } = React;
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
  const MSG_CAP = 5;

  // ── URL helpers ───────────────────────────────────────────
  function normalizeUrl(raw) {
    let s = (raw || "").trim();
    if (!s) return "";
    if (!/^https?:\/\//i.test(s)) s = "https://" + s;
    return s.replace(/\/+$/, "");
  }
  function validUrl(url) {
    try {
      const u = new URL(url);
      return /^[^.\s]+\.[^.\s]{2,}/.test(u.hostname.replace(/^www\./, ""));
    } catch (e) { return false; }
  }
  function domainFromUrl(url) {
    try { return new URL(url).hostname.replace(/^www\./, ""); }
    catch (e) { return url; }
  }

  // ── Persistence (website = unique key) ─────────────────────
  const KEY = (dom) => "ezt-byo:" + dom;
  const TRIES_KEY = (dom) => "ezt-byo-tries:" + dom;
  function loadSaved(dom) {
    try { const v = localStorage.getItem(KEY(dom)); return v ? JSON.parse(v) : null; }
    catch (e) { return null; }
  }
  function persist(dom, data) {
    try { localStorage.setItem(KEY(dom), JSON.stringify(data)); } catch (e) {}
  }
  function clearSaved(dom) {
    try { localStorage.removeItem(KEY(dom)); } catch (e) {}
  }
  function getTries(dom) {
    try { return Number(localStorage.getItem(TRIES_KEY(dom)) || "0"); }
    catch (e) { return 0; }
  }
  function bumpTries(dom) {
    const n = getTries(dom) + 1;
    try { localStorage.setItem(TRIES_KEY(dom), String(n)); } catch (e) {}
    return n;
  }
  function resetTries(dom) {
    try { localStorage.removeItem(TRIES_KEY(dom)); } catch (e) {}
  }

  // ── Claude helpers ─────────────────────────────────────────
  // ╭───────────────────────────────────────────────────────────────╮
  // │ AGENT CONFIG - edit these when wiring up your own Anthropic key │
  // ╰───────────────────────────────────────────────────────────────╯
  // ask() below is the ONE place the agent talks to Claude. In this
  // preview it uses the built-in window.claude.complete. When you deploy,
  // stand up a tiny backend that holds your Anthropic API key SERVER-SIDE
  // and returns JSON { text: "..." }, then point AGENT_BACKEND_URL at it.
  // NEVER paste your API key in this file - it ships to the browser and
  // anyone could read it and run up your bill. The key lives only on the
  // server. (See "Claude Code - Integration Guide.md" in the project root.)
  const AGENT_BACKEND_URL = "/api/agent";

  // Restrictions for the demo agent - appended to every reply prompt.
  // Add topic limits, tone, "don't discuss X", length caps, etc.
  const AGENT_CONSTRAINTS = [
    "Only answer questions related to this business and what it offers; politely decline anything off-topic.",
    "Never state a hard price, exact availability, or legal/medical/financial specifics - offer to confirm instead.",
    "Keep replies under 320 characters.",
    "Never use em dashes or en dashes; if you need a dash, use a regular hyphen (-).",
  ].join(" ");

  const hasAI = () => !!(window.claude && window.claude.complete);
  async function ask(prompt, ms = 24000) {
    const t = new Promise((_, rej) => setTimeout(() => rej(new Error("timeout")), ms));
    let call;
    if (AGENT_BACKEND_URL) {
      // Your backend proxies this prompt to Anthropic (key server-side) and
      // responds with { text }. See the integration guide for a sample.
      call = fetch(AGENT_BACKEND_URL, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ prompt }),
      }).then((r) => r.json()).then((d) => d.text || d.completion || "");
    } else if (hasAI()) {
      call = window.claude.complete(prompt);
    } else {
      return Promise.reject(new Error("no-api"));
    }
    return Promise.race([call, t]);
  }
  function parseJSON(s) {
    if (!s) return null;
    let txt = String(s).trim();
    const fence = txt.match(/```(?:json)?\s*([\s\S]*?)```/i);
    if (fence) txt = fence[1].trim();
    const a = txt.indexOf("{"), b = txt.lastIndexOf("}");
    if (a >= 0 && b > a) txt = txt.slice(a, b + 1);
    try { return JSON.parse(txt); } catch (e) { return null; }
  }

  function fallbackProfile(url) {
    const dom = domainFromUrl(url);
    const stem = dom.split(".")[0].replace(/[-_]+/g, " ");
    const name = stem.replace(/\b\w/g, (c) => c.toUpperCase());
    return {
      businessName: name || "Your Business",
      category: "Local business",
      emoji: "🏪",
      description: "A local business at " + dom + ". The agent has read your site and is ready to answer customer questions about what you offer.",
      location: "",
      previewLines: [
        "Found your homepage and main navigation",
        "Identified your business name: " + name,
        "Picked up your contact section",
        "Indexed your services and about copy",
        "Mapped your most common customer questions",
      ],
      known: [
        { label: "Business name", value: name },
        { label: "What you do", value: "From your site" },
        { label: "How to reach you", value: "Contact details" },
        { label: "Services overview", value: "Indexed" },
        { label: "Common questions", value: "Ready to answer" },
      ],
    };
  }

  // Keyword → emoji map so a relevant contact picture always resolves,
  // even for profiles saved before the emoji field existed.
  const EMOJI_MAP = [
    [/stump|tree|arbor|landscap|lawn|garden|nursery/, "🌳"],
    [/plumb|drain|pipe|sewer/, "🚿"],
    [/electric|electrician|wiring/, "💡"],
    [/roof|gutter/, "🏠"],
    [/hvac|heating|cooling|air condition/, "❄️"],
    [/clean|maid|janitor/, "🧽"],
    [/pest|exterminat/, "🐜"],
    [/paint|drywall/, "🎨"],
    [/construct|contractor|build|remodel|renovat|handyman/, "🔨"],
    [/concrete|paving|masonry|fence|deck/, "🧱"],
    [/pool|spa/, "🏊"],
    [/auto|car|mechanic|tire|garage|detail/, "🚗"],
    [/tow/, "🚚"],
    [/barber|salon|hair|stylist/, "💈"],
    [/nail|spa|beauty|esthetic|lash|brow/, "💅"],
    [/tattoo/, "🖊️"],
    [/yoga|pilates/, "🧘"],
    [/gym|fitness|crossfit|train/, "🏋️"],
    [/dent/, "🦷"],
    [/chiro|physio|therap|massage/, "💆"],
    [/vet|animal|pet|groom/, "🐾"],
    [/doctor|clinic|medical|health|derma/, "🩺"],
    [/optom|eye|vision/, "👓"],
    [/law|legal|attorney|firm/, "⚖️"],
    [/account|tax|bookkeep|financ/, "🧾"],
    [/real estate|realty|realtor|property|mortgage/, "🏡"],
    [/insur/, "🛡️"],
    [/photo|video|film/, "📷"],
    [/cafe|coffee|espresso/, "☕"],
    [/bakery|bake|pastry|cake/, "🧁"],
    [/pizza/, "🍕"],
    [/restaurant|kitchen|grill|diner|eatery|bistro|bar /, "🍽️"],
    [/cater/, "🍱"],
    [/bar|brew|pub|wine|liquor/, "🍺"],
    [/florist|flower/, "💐"],
    [/jewel/, "💍"],
    [/boutique|apparel|cloth|fashion/, "👗"],
    [/book/, "📚"],
    [/pharm/, "💊"],
    [/grocery|market|deli/, "🛒"],
    [/hotel|inn|lodge|motel/, "🏨"],
    [/travel|tour/, "✈️"],
    [/move|moving|haul/, "📦"],
    [/locksmith|lock/, "🔑"],
    [/security|alarm/, "🔒"],
    [/solar|energy/, "🔆"],
    [/school|tutor|academy|learn|educat/, "🎓"],
    [/music|band|dj/, "🎵"],
    [/event|wedding|party/, "🎉"],
    [/church|ministry/, "⛪"],
    [/tech|software|web|digital|marketing|agency|design|consult/, "💻"],
    [/laundry|dry clean/, "👕"],
    [/shoe|cobbler/, "👟"],
    [/storage/, "🗄️"],
  ];
  function pickEmoji(profile) {
    const hay = ((profile.businessName || "") + " " + (profile.category || "") + " " + (profile.description || "")).toLowerCase();
    for (const [re, em] of EMOJI_MAP) { if (re.test(hay)) return em; }
    return "🏪";
  }
  function ensureEmoji(profile) {
    if (profile && (!profile.emoji || typeof profile.emoji !== "string" || !profile.emoji.trim())) {
      profile.emoji = pickEmoji(profile);
    }
    return profile;
  }

  // ── Demo session logging ─────────────────────────────────────
  // Each visit to /try gets one synthetic sessionId; every customer text and
  // agent reply is logged so the operator can review them in the portal under
  // a special "Demo" agent.
  const DEMO_SESSION_ID = (() => {
    try {
      const k = "ezt-byo-session";
      let id = sessionStorage.getItem(k);
      if (!id) {
        id = "byo-" + Date.now().toString(36) + "-" + Math.random().toString(36).slice(2, 8);
        sessionStorage.setItem(k, id);
      }
      return id;
    } catch (e) {
      return "byo-" + Date.now().toString(36);
    }
  })();
  // Serialize log posts so the customer + agent turns for the same session
  // never race each other on the GitHub-backed session file (two concurrent
  // PUTs would collide on SHA and silently drop one turn).
  let _demoLogQueue = Promise.resolve();
  function logDemoTurn(domain, from, text) {
    if (!domain || !from || !text) return;
    const payload = JSON.stringify({ sessionId: DEMO_SESSION_ID, domain, from, text });
    _demoLogQueue = _demoLogQueue.then(() =>
      fetch("/api/demo-log", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: payload,
        keepalive: true,
      }).catch(() => {}),
    );
  }

  // Fetch the actual website contents via the server proxy so the agent's
  // answers are grounded in the real site, not inferred from the domain.
  async function scrapeSite(url) {
    try {
      const res = await fetch("/api/scrape", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ url }),
      });
      const d = await res.json().catch(() => ({}));
      return { text: d.text || "", title: d.title || "" };
    } catch (e) {
      return { text: "", title: "" };
    }
  }

  function siteBlock(siteText, siteTitle) {
    if (!siteText) return "";
    const titleLine = siteTitle ? `Site title: ${siteTitle}\n` : "";
    return `\nActual website contents (read from the site just now - treat as the ground truth about this business):\n${titleLine}"""\n${siteText}\n"""\n`;
  }

  async function fetchProfile(url, siteText, siteTitle) {
    const prompt =
`You are the onboarding engine for an AI text-message assistant that small businesses use to auto-answer customer SMS. A business owner just entered their website so we can build a working agent from it. Use the site contents below as the primary source of truth. If something isn't on the site, fall back to sensible general knowledge for this kind of business. Never refuse - this is a live demo and approximation is fine.

Website: ${url}${siteBlock(siteText, siteTitle)}
Return ONLY minified JSON (no prose, no markdown) with exactly this shape:
{"businessName": string, "category": short string, "emoji": "a single emoji that best represents this business type", "description": "1-2 sentence plain summary of what the business does", "location": "City, ST or empty string", "previewLines": [5-6 short strings, each a real snippet you found on the site], "known": [5-6 objects {"label": short, "value": short} the agent confidently knows and can answer about - pull these from the site copy when possible]}

Sentence case. No emoji except in the "emoji" field. Keep every string short.`;
    const raw = await ask(prompt);
    const p = parseJSON(raw);
    if (!p || !p.businessName) throw new Error("bad-profile");
    if (!p.emoji) p.emoji = "🏪";
    p.previewLines = Array.isArray(p.previewLines) ? p.previewLines.slice(0, 6) : [];
    p.known = Array.isArray(p.known) ? p.known.slice(0, 5) : [];
    return p;
  }

  async function introduceAs(profile, known, siteText, siteTitle) {
    const prompt =
`You are the AI text-message assistant for "${profile.businessName}"${profile.location ? " in " + profile.location : ""}. A customer just texted: "Is this ${profile.businessName}?"

About the business:
${profile.description}
Services / what you know:
${known.map((k) => "- " + k.label + (k.value ? ": " + k.value : "")).join("\n")}
${siteBlock(siteText, siteTitle)}
Reply over SMS: confirm yes it's them, introduce the business warmly in one line, mention one or two of the main services you actually saw on the site, then ask how you can help. 2-3 short sentences. Natural texting tone, sentence case, no emoji. Extra rules: ${AGENT_CONSTRAINTS} Return ONLY the reply text.`;
    const raw = await ask(prompt);
    let reply = String(raw || "").trim().replace(/^["']|["']$/g, "").replace(/[—–]/g, "-");
    if (!reply) throw new Error("bad-intro");
    return reply;
  }

  async function answerAs(profile, known, transcript, text, siteText, siteTitle) {
    const prompt =
`You are the AI text-message assistant for "${profile.businessName}"${profile.location ? " in " + profile.location : ""}. Reply to the customer's text the way a sharp, friendly small business would over SMS.

About the business:
${profile.description}
What you know:
${known.map((k) => "- " + k.label + (k.value ? ": " + k.value : "")).join("\n")}
${siteBlock(siteText, siteTitle)}
Rules:
- 1-2 short sentences. Warm, natural texting tone. Sentence case. No emoji.
- Ground your answer in the site contents above whenever the question is covered there. If the site doesn't cover it, give a helpful general answer for this type of business and offer to confirm the exact detail.
- Do not invent a hard specific you can't know (an exact price, an exact address, a named staff member) unless it appears on the site.
- Extra rules: ${AGENT_CONSTRAINTS}

Conversation so far:
${transcript || "(none)"}

Customer's latest text: "${text}"

Return ONLY the reply text. No JSON, no quotes, no label.`;
    const raw = await ask(prompt);
    let reply = String(raw || "").trim().replace(/^["']|["']$/g, "").replace(/[—–]/g, "-");
    if (!reply) throw new Error("bad-answer");
    return reply;
  }

  // ── Glyphs ─────────────────────────────────────────────────
  const Check = () => (<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3.4" strokeLinecap="round" strokeLinejoin="round"><path d="M20 6L9 17l-5-5"></path></svg>);
  const Arrow = () => (<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round"><path d="M5 12h14M13 6l6 6-6 6"></path></svg>);
  const ArrowUp = () => (<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round"><path d="M12 19V5M6 11l6-6 6 6"></path></svg>);
  const Caret = () => (<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round"><path d="M6 9l6 6 6-6"></path></svg>);
  const ChatGlyph = () => (<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"></path></svg>);

  // ── Pulse the header "Get started" button ──────────────────
  function setHeaderPulse(on) {
    const btn = document.querySelector(".nav .js-get-started");
    if (btn) btn.classList.toggle("btn-pulse", !!on);
  }

  // Render a string with the phrase "Get started" bolded.
  function boldGetStarted(text) {
    const parts = String(text).split("Get started");
    const out = [];
    parts.forEach((p, i) => {
      if (i > 0) out.push(<strong key={"b" + i}>Get started</strong>);
      out.push(<React.Fragment key={"t" + i}>{p}</React.Fragment>);
    });
    return out;
  }

  // ══════════════════════════════════════════════════════════
  function BYOApp() {
    const [phase, setPhase] = useState("input");      // input | loading | ready
    const [draftUrl, setDraftUrl] = useState("");
    const [website, setWebsite] = useState("");
    const [err, setErr] = useState("");
    const [restoring, setRestoring] = useState(false);

    const [loadStep, setLoadStep] = useState(0);
    const [preview, setPreview] = useState([]);

    const [profile, setProfile] = useState(null);
    const [known, setKnown] = useState([]);
    const [siteText, setSiteText] = useState("");
    const [siteTitle, setSiteTitle] = useState("");

    const [messages, setMessages] = useState([]);
    const [typing, setTyping] = useState(false);
    const [chatBusy, setChatBusy] = useState(false);
    const [capped, setCapped] = useState(false);
    const [detailsOpen, setDetailsOpen] = useState(false);
    const [isMobile, setIsMobile] = useState(
      typeof window !== "undefined" && window.matchMedia
        ? window.matchMedia("(max-width: 768px)").matches
        : false,
    );
    useEffect(() => {
      if (typeof window === "undefined" || !window.matchMedia) return;
      const mq = window.matchMedia("(max-width: 768px)");
      const onChange = () => setIsMobile(mq.matches);
      mq.addEventListener ? mq.addEventListener("change", onChange) : mq.addListener(onChange);
      return () => {
        mq.removeEventListener ? mq.removeEventListener("change", onChange) : mq.removeListener(onChange);
      };
    }, []);

    const queueRef = useRef([]);
    const processingRef = useRef(false);

    const msgRef = useRef([]);
    useEffect(() => { msgRef.current = messages; }, [messages]);
    useEffect(() => () => setHeaderPulse(false), []);
    // When the agent goes live on mobile, scroll the visitor to the top so
    // they land on the iPhone + "messages left" counter as their first view,
    // then auto-send "Is this {biz}?" so the conversation kicks off itself.
    // Auto messages are marked with .auto and don't count toward MSG_CAP.
    const introSentRef = useRef(false);
    useEffect(() => {
      if (phase !== "ready" || !isMobile || introSentRef.current || !profile) return;
      try { window.scrollTo({ top: 0, behavior: "auto" }); } catch (e) {}
      introSentRef.current = true;
      setTimeout(() => {
        send("Is this " + profile.businessName + "?", { intro: true, auto: true });
      }, 700);
    }, [phase, isMobile, profile]);

    const dom = website ? domainFromUrl(website) : "";
    // Auto-intro messages don't count toward the visitor's 5-message budget.
    const sentCount = messages.filter((m) => m.from === "me" && !m.auto).length;
    const messaging = sentCount > 0;
    const DISCLAIMER = "This preview only knows a small sample of your website, so answers are not production-ready and will be improved later. Your launched agent trains on much more - past chats, docs, and your own answers. It's easy and we can set it up with you.";

    const LOAD_STEPS = restoring
      ? ["Recognised " + dom, "Catching up on your pages", "Re-reading your pages", "Rebuilding your agent"]
      : ["Connecting to " + dom, "Reading every page and link", "Extracting services, hours and details", "Building the most complete agent it can"];

    async function train(rawUrl) {
      const url = normalizeUrl(rawUrl);
      if (!validUrl(url)) { setErr("Enter a real website, like yourbusiness.com"); return; }
      setErr("");
      const d = domainFromUrl(url);
      const saved = loadSaved(d);
      setWebsite(url);
      setRestoring(!!saved);
      setPreview([]);
      setLoadStep(0);
      setHeaderPulse(false);
      setPhase("loading");

      // Kick off scrape + profile in parallel. Profile waits for scrape text.
      const sitePromise = saved && saved.siteText
        ? Promise.resolve({ text: saved.siteText, title: saved.siteTitle || "" })
        : scrapeSite(url);

      const profileP = saved
        ? Promise.resolve({ profile: saved.profile, generated: true })
        : sitePromise.then(async (s) => {
            try {
              const profile = await fetchProfile(url, s.text, s.title);
              return { profile, generated: true };
            } catch (e) {
              return { profile: fallbackProfile(url), generated: false };
            }
          });

      const fast = !!saved;
      setLoadStep(0); await sleep(fast ? 480 : 750);
      setLoadStep(1); await sleep(fast ? 560 : 900);
      const [profileResult, site] = await Promise.all([profileP, sitePromise, sleep(fast ? 500 : 1150)]);
      const prof = profileResult.profile;
      ensureEmoji(prof);
      setPreview(prof.previewLines || []);
      setLoadStep(2); await sleep(fast ? 650 : 1250);
      setLoadStep(3); await sleep(fast ? 600 : 1050);

      const k = saved ? saved.known : (prof.known || []);

      // Quality check: poor scrape (too little real text) OR profile generation failed.
      const textLen = (site.text || "").length;
      const poorScrape = textLen < 300;
      const poorProfile = !profileResult.generated;
      const isPoor = !saved && (poorScrape || poorProfile);

      if (isPoor) {
        const tries = bumpTries(d);
        clearSaved(d);
        if (tries >= 3) {
          setPhase("blocked");
        } else {
          setProfile(prof);
          setKnown(k);
          setSiteText(site.text || "");
          setSiteTitle(site.title || "");
          setPhase("poor");
        }
        return;
      }

      // Good profile - keep it and reset the try counter for this domain.
      resetTries(d);
      setProfile(prof);
      setKnown(k);
      setSiteText(site.text || "");
      setSiteTitle(site.title || "");
      setMessages([]);
      setCapped(false);
      persist(d, {
        website: url,
        profile: prof,
        known: k,
        siteText: site.text || "",
        siteTitle: site.title || "",
      });
      setPhase("ready");
    }

    function retryDifferentUrl() {
      setPhase("input");
      setDraftUrl("");
      setProfile(null);
      setKnown([]);
      setSiteText("");
      setSiteTitle("");
    }
    function retrySameUrl() {
      const u = website;
      setPhase("input");
      train(u);
    }

    // Queue messages so the visitor can keep typing while a reply generates.
    function send(text, opts = {}) {
      const t = (text || "").trim();
      if (!t || capped) return;
      // Only non-auto messages count toward MSG_CAP.
      if (!opts.auto) {
        const already = msgRef.current.filter((m) => m.from === "me" && !m.auto).length
          + queueRef.current.filter((q) => !q.opts.auto).length;
        if (already >= MSG_CAP) return;
      }
      queueRef.current.push({ text: t, opts });
      pumpQueue();
    }

    async function pumpQueue() {
      if (processingRef.current) return;
      const item = queueRef.current.shift();
      if (!item) return;
      processingRef.current = true;
      await processMessage(item.text, item.opts);
      processingRef.current = false;
      pumpQueue();
    }

    async function processMessage(t, opts) {
      // Auto messages don't count toward the visitor's 5-message demo cap.
      const priorSent = msgRef.current.filter((m) => m.from === "me" && !m.auto).length;
      const isLast = !opts.auto && (priorSent + 1 >= MSG_CAP);
      setMessages((m) => [...m, { from: "me", text: t, auto: !!opts.auto }]);
      setChatBusy(true);
      setTyping(true);
      const transcript = msgRef.current.slice(-6)
        .map((m) => (m.from === "me" ? "Customer: " : "Agent: ") + m.text).join("\n");
      let reply;
      try {
        reply = opts.intro
          ? await introduceAs(profile, known, siteText, siteTitle)
          : await answerAs(profile, known, transcript, t, siteText, siteTitle);
      } catch (e) {
        reply = "Thanks for reaching out! Let me have someone from the team follow up with that exact detail shortly.";
      }
      await sleep(280);
      setTyping(false);
      setMessages((m) => [...m, { from: "them", text: reply }]);
      setChatBusy(false);
      // Log this exchange to the Demo agent's conversation store (god-visible portal).
      logDemoTurn(dom, "customer", t);
      logDemoTurn(dom, "agent", reply);

      if (isLast) {
        queueRef.current = [];
        await sleep(900);
        setTyping(true);
        await sleep(1100);
        setTyping(false);
        setMessages((m) => [...m, {
          from: "them", system: true,
          text: "That's the demo - thanks for trying me out! I was built from your site in seconds. With our platform and onboarding, I can be taught and trained to answer every customer exactly the way you would. Tap Get started up top to make me yours.",
        }]);
        setCapped(true);
        setHeaderPulse(true);
      }
    }

    // ── Left card by phase ──────────────────────────────────
    let left;
    if (phase === "input") {
      left = (
        <div className="byo-card" data-screen-label="BYO - paste website">
          <div className="byo-card-head">
            <h2>Build an agent from your website</h2>
            <p>Paste your website. We'll read it, pull out what we can, and stand up a working text agent you can message in seconds, just as a customer would - no signup needed.</p>
          </div>
          <form className="byo-form" onSubmit={(e) => { e.preventDefault(); train(draftUrl); }}>
            <div className="byo-url-wrap">
              <input className="field byo-url-field" type="text" inputMode="url" autoComplete="url"
                autoCapitalize="off" autoCorrect="off" spellCheck="false"
                placeholder="yourbusiness.com" value={draftUrl}
                onChange={(e) => { setDraftUrl(e.target.value); if (err) setErr(""); }} />
              <button type="submit" className="btn btn-primary">Try my agent →</button>
            </div>
            <span className="byo-err">{err}</span>
          </form>
        </div>
      );
    } else if (phase === "blocked") {
      left = (
        <div className="byo-card" data-screen-label="BYO - blocked">
          <div className="byo-card-head">
            <h2>We couldn't read {dom}</h2>
            <p>The site is configured in a way our quick demo crawl can't get through right now. That's a demo limit, not a product one - when you sign up, we work with you directly to make sure your agent has every detail it needs.</p>
          </div>
          <div style={{ display: "flex", gap: 10, marginTop: 18 }}>
            <button type="button" className="btn btn-primary" onClick={retryDifferentUrl}>Try a different site</button>
            <a href="/get-started" className="btn btn-secondary">Get started →</a>
          </div>
        </div>
      );
    } else if (phase === "poor") {
      const remaining = Math.max(0, 3 - getTries(dom));
      left = (
        <div className="byo-card" data-screen-label="BYO - poor scrape">
          <div className="byo-card-head">
            <h2>That didn't pick up enough about {dom}</h2>
            <p>We could only see generic details - the site may be JavaScript-rendered or rate-limiting us. Want to retry, or use a different URL?</p>
          </div>
          <div style={{ display: "flex", gap: 10, marginTop: 18, flexWrap: "wrap" }}>
            <button type="button" className="btn btn-primary" onClick={retrySameUrl}>Try again ({remaining} left)</button>
            <button type="button" className="btn btn-secondary" onClick={retryDifferentUrl}>Use a different URL</button>
          </div>
        </div>
      );
    } else if (phase === "loading") {
      left = (
        <div className="byo-card" data-screen-label="BYO - building">
          <div className="byo-card-head">
            <h2>{restoring ? "Welcome back" : "Reading your website"}</h2>
            <p>{restoring ? "Restoring the agent built for this site." : "This is what EZText does automatically the moment you connect a site."}</p>
          </div>
          <span className="load-domain">{dom}</span>
          <div className="load-steps">
            {LOAD_STEPS.map((s, i) => (
              <div key={i} className={"load-step" + (i === loadStep ? " active" : "") + (i < loadStep ? " done" : "")}>
                <span className="load-dot"><span className="load-check"><Check /></span></span>
                <span>{s}{i === loadStep ? "…" : ""}</span>
              </div>
            ))}
          </div>
          {preview.length > 0 && (
            <div className="load-preview">
              <span className="load-preview-label">Found on your site</span>
              {preview.map((p, i) => (
                <span className="preview-line" key={i} style={{ animationDelay: (i * 0.1) + "s" }}>
                  <Check />{p}
                </span>
              ))}
            </div>
          )}
        </div>
      );
    } else {
      // Top bubble = business (constant). Second bubble swaps from the
      // "live, text it" prompt to the preview alert + disclaimer after the
      // first message is sent. The business bubble never changes.
      const checklistItems = (
        <div className="checklist">
          {known.map((k, i) => (
            <div className="check-item known" key={"k" + i}>
              <span className="check-mark"><Check /></span>
              <span className="check-label">{k.label}</span>
              {k.value && <span className="check-value">{k.value}</span>}
            </div>
          ))}
        </div>
      );
      // On mobile, details collapse-by-default at all times (so the page stays
      // short and uncluttered). On desktop, details stay expanded until the
      // first user message.
      const collapseDetails = isMobile || messaging;

      const businessBubble = (
        <div className="byo-win byo-win-biz" data-screen-label="BYO - business">
          <div className="biz-top">
            <span className="biz-logo biz-logo-emoji">{profile.emoji}</span>
            <div>
              <div className="biz-name">{profile.businessName}</div>
              <div className="biz-cat">{profile.category}</div>
            </div>
          </div>
          <p className="biz-desc">{profile.description}</p>
          {collapseDetails ? (
            <React.Fragment>
              <button type="button" className={"byo-details-toggle" + (detailsOpen ? " open" : "")}
                onClick={() => setDetailsOpen((o) => !o)} aria-expanded={detailsOpen}>
                <span>What your agent picked up</span>
                <span className="byo-details-caret"><Caret /></span>
              </button>
              {detailsOpen && (
                <div className="byo-details-inner">
                  {profile.location && <span className="biz-loc"><Arrow />{profile.location}</span>}
                  {checklistItems}
                </div>
              )}
            </React.Fragment>
          ) : (
            <React.Fragment>
              {profile.location && <span className="biz-loc"><Arrow />{profile.location}</span>}
              <div className="byo-divider"></div>
              <div>
                <div className="checklist-head">What your agent picked up</div>
                {checklistItems}
              </div>
            </React.Fragment>
          )}
        </div>
      );

      // Always-visible "Answers may be inaccurate" / "Preview only" card.
      const alertWin = (
        <div className="byo-win byo-alert-win" data-screen-label="BYO - preview note">
          <div className="byo-flag">
            <span className="byo-flag-mark">!</span>
            <span className="byo-flag-text">Answers may be inaccurate right now - that's corrected when you set the agent up.</span>
          </div>
          <div className="byo-divider"></div>
          <div className="byo-disc">
            <span className="byo-win-label">Preview only</span>
            <p>{DISCLAIMER}</p>
          </div>
        </div>
      );

      // The auto-intro prompt ("Your agent sample is live. Text it →") is
      // hidden on mobile. Mobile users get the alert card from the start.
      left = (
        <div className="byo-stack" data-screen-label="BYO - agent ready">
          {businessBubble}
          {isMobile || messaging ? alertWin : (
            <div className="byo-prompt" data-screen-label="BYO - live prompt">
              <button type="button" className="byo-prompt-arrow"
                onClick={() => send("Is this " + profile.businessName + "?", { intro: true })}
                disabled={chatBusy} aria-label={"Text " + profile.businessName}>
                <Arrow />
              </button>
              <div>
                <strong>Your agent sample is live. Text it →</strong>
                <span>Tap the arrow to send your first text or type your own - ask {profile.businessName} about hours, pricing..anything.</span>
              </div>
            </div>
          )}
        </div>
      );
    }

    // ── Right phone ─────────────────────────────────────────
    const ready = phase === "ready";
    let phoneBody;
    if (ready) {
      phoneBody = (
        <React.Fragment>
          {messages.length === 0 && !typing
            ? <div className="start-hint">Text your agent like a customer would - ask about hours, pricing, anything.</div>
            : <div className="chat-stamp">{nowStamp()}</div>}
          {messages.map((m, i) => (
            m.system
              ? <div className="chat-end-note" key={i}>{boldGetStarted(m.text)}</div>
              : <ChatBubble key={i} from={m.from}>{m.text}</ChatBubble>
          ))}
          {typing && <TypingBubble from="them" />}
        </React.Fragment>
      );
    } else {
      phoneBody = (
        <div className={"phone-idle" + (phase === "loading" ? " loading" : "")}>
          <span className="idle-glyph"><ChatGlyph /></span>
          <span className="idle-title">{phase === "loading" ? "Building your agent…" : "Your agent will appear here"}</span>
          <span className="idle-sub">{phase === "loading" ? "Hang tight - it's reading your site." : "Paste your website to spin up a live agent you can text."}</span>
        </div>
      );
    }

    const phoneContact = ready ? profile.businessName : "Your agent";
    const phoneAvatar = ready ? profile.emoji : "AI";
    const composePlaceholder = capped ? "Demo complete - tap Get started" : "Text message";

    return (
      <div className="byo-grid">
        <div className="byo-intro">
          <h1 className="byo-title">Try the agent on your own website</h1>
        </div>
        {left}
        <div className="byo-right">
          <div style={{ width: 360, height: 548, flexShrink: 0 }}>
            <IOSDevice width={360} height={548} time={nowTime()} homeIndicator={false}
              radius={38} island={{ width: 96, height: 26, top: 8 }} statusCompact>
              <ChatScreen contact={phoneContact} avatar={phoneAvatar} tone="plain"
                callIcon compact threadRef={null}
                footer={ready ? <PhoneCompose onSend={send} busy={chatBusy} disabled={capped} placeholder={composePlaceholder} /> : null}>
                {phoneBody}
              </ChatScreen>
            </IOSDevice>
          </div>
          {ready && !capped && (
            <div className="byo-counter">
              <span className="byo-counter-dot"></span>
              {MSG_CAP - sentCount} {MSG_CAP - sentCount === 1 ? "message" : "messages"} left in the demo
            </div>
          )}
        </div>
      </div>
    );
  }

  // Interactive iMessage compose (auto-scrolls its own thread).
  // Stays editable while a reply generates - only locks when the demo is done.
  function PhoneCompose({ onSend, busy, disabled, placeholder }) {
    const [val, setVal] = useState("");
    const [focused, setFocused] = useState(false);
    const off = disabled;                          // capped only - typing allowed while busy
    const showCaret = !val && !off && !focused;    // empty + messages still allotted
    const submit = (e) => {
      e.preventDefault();
      const t = val.trim();
      if (!t || off) return;
      onSend(t);
      setVal("");
    };
    return (
      <form className={"bcompose" + (disabled ? " bcompose-done" : "") + (showCaret ? " bcompose-invite" : "")} onSubmit={submit}>
        <div className="bcompose-field">
          <input className="bcompose-input" value={val} disabled={off}
            placeholder={showCaret ? "" : (placeholder || "Text message")} aria-label="Message your agent"
            onFocus={() => setFocused(true)} onBlur={() => setFocused(false)}
            onChange={(e) => setVal(e.target.value)} />
          {showCaret && (
            <span className="bcompose-faux" aria-hidden="true">
              <span className="bcompose-caret"></span>Text message
            </span>
          )}
        </div>
        <button type="submit" className={"bcompose-send" + (val.trim() && !off ? " active" : "")}
          disabled={!val.trim() || off} aria-label="Send">
          <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round">
            <path d="M12 19V5"></path><path d="M5 12l7-7 7 7"></path>
          </svg>
        </button>
      </form>
    );
  }

  return BYOApp;
})();

// Auto-scroll the phone thread on new messages.
(function () {
  const obsTarget = () => document.querySelector("#byo-root .chat-thread");
  const tick = () => {
    const el = obsTarget();
    if (el) el.scrollTop = el.scrollHeight;
  };
  const mo = new MutationObserver(tick);
  const start = () => {
    const el = obsTarget();
    if (el) { mo.observe(el, { childList: true, subtree: true }); tick(); }
    else setTimeout(start, 300);
  };
  start();
})();

const byoRoot = document.getElementById("byo-root");
if (byoRoot) ReactDOM.createRoot(byoRoot).render(<BYO />);
