// BeaconChat.jsx - shared Beacon chat session.
// This keeps Claude's UI shell, but connects the conversation to the secure
// Siteworks backend instead of using canned demo replies in the browser.

const BEACON_CONFIG = {
  endpoint: "/api/siteworks-chat",
  bookingUrl: "https://calendly.com/taurist/siteworks",
  assistantName: "Beacon",
  enableRemote: typeof window !== "undefined" && window.location.protocol !== "file:",
  ...(window.SITEWORKS_CHAT_CONFIG || {})
};

const BEACON_FRAMES = [
  "assets/beacon-think-1.png",
  "assets/beacon-think-2.png",
  "assets/beacon-think-3.png",
  "assets/beacon-think-4.png",
];

const BEACON_THINK_LABELS = [
  "Reading your message",
  "Checking the context",
  "Shaping the next step",
  "Almost ready"
];

const BEACON_SESSION_KEY = "siteworks_beacon_session_v1";
const BEACON_SESSION_TTL_MS = 1000 * 60 * 60 * 24 * 10;
const BCN_USD = (n) => "$" + Math.round(Number(n || 0)).toLocaleString("en-US");
const BeaconChatContext = React.createContext(null);
window.BeaconChatContext = BeaconChatContext;

function useBeaconChat() {
  return React.useContext(BeaconChatContext);
}

function wait(ms) {
  return new Promise((resolve) => window.setTimeout(resolve, ms));
}

function uid(prefix = "msg") {
  return `${prefix}_${Date.now()}_${Math.random().toString(16).slice(2)}`;
}

function createSessionId(forceNew = false) {
  const key = "siteworks_beacon_session_id";
  const createdKey = "siteworks_beacon_session_created_at";
  const fallbackId = uid("sw");
  try {
    const existing = window.localStorage.getItem(key);
    const createdAt = Number(window.localStorage.getItem(createdKey) || 0);
    if (existing && createdAt && Date.now() - createdAt < BEACON_SESSION_TTL_MS && !forceNew) return existing;
    const next = window.crypto && window.crypto.randomUUID ? window.crypto.randomUUID() : fallbackId;
    window.localStorage.setItem(key, next);
    window.localStorage.setItem(createdKey, String(Date.now()));
    return next;
  } catch (error) {
    return fallbackId;
  }
}

function loadBeaconSession() {
  try {
    const raw = window.localStorage.getItem(BEACON_SESSION_KEY);
    if (!raw) return null;
    const saved = JSON.parse(raw);
    if (!saved || !saved.expiresAt || Date.now() > saved.expiresAt) {
      window.localStorage.removeItem(BEACON_SESSION_KEY);
      return null;
    }
    if (!Array.isArray(saved.messages) || !saved.messages.length) return null;
    return saved;
  } catch (error) {
    return null;
  }
}

function saveBeaconSession(snapshot) {
  try {
    window.localStorage.setItem(BEACON_SESSION_KEY, JSON.stringify({
      ...snapshot,
      savedAt: Date.now(),
      expiresAt: Date.now() + BEACON_SESSION_TTL_MS
    }));
  } catch (error) {
    // Persistence is a convenience. Beacon should keep working if storage is full.
  }
}

function guestInitialFrom(text) {
  const m = String(text || "").match(/\b(?:i am|i'm|im|my name is|name is|name's|this is|it is|it's|call me|i am called|i'm called)\s+([A-Za-z][A-Za-z'’-]{1,})/i);
  if (m) return m[1][0].toUpperCase();
  const t = String(text || "").trim();
  if (/^[A-Za-z][A-Za-z'’-]{1,19}$/.test(t)) {
    const stop = new Set(["yes", "no", "yeah", "yep", "nope", "ok", "okay", "hi", "hello", "hey", "sure", "thanks", "thank", "maybe", "nothing", "none", "help", "yo"]);
    if (!stop.has(t.toLowerCase())) return t[0].toUpperCase();
  }
  return null;
}

function normalizeUrl(url) {
  const clean = String(url || "").trim().replace(/[),.;]+$/g, "");
  if (!clean) return "";
  return /^https?:\/\//i.test(clean) ? clean : `https://${clean}`;
}

function looksLikeName(text) {
  const clean = String(text || "").trim().replace(/[.!?]+$/g, "");
  if (!clean || clean.length > 42) return false;
  if (/@|https?:|www\.|\.(com|co|net|org|io|ai)\b/i.test(clean)) return false;
  if (/\b(site|website|business|company|pricing|cost|page|pages|need|want|looking|help|build|current|offer|service)\b/i.test(clean)) return false;
  return /^[a-z][a-z.'-]*(?:\s+[a-z][a-z.'-]*){0,2}$/i.test(clean);
}

function applyLocalLeadFields(text, currentFields, stage) {
  const next = { ...(currentFields || {}) };
  const raw = String(text || "");
  const lower = raw.toLowerCase();
  const emailMatch = raw.match(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/i);
  const urlMatch = raw.match(/\b(?:https?:\/\/|www\.)[^\s<]+|\b[a-z0-9-]+\.(?:com|co|net|org|io|ai|us|biz|studio|agency|marketing|design)(?:\/[^\s<]*)?/i);
  const nameMatch = raw.match(/\b(?:my name is|call me|i am|i'm)\s+([a-z][a-z.'-]*(?:\s+[a-z][a-z.'-]*){0,2})/i);

  if (emailMatch) next.email = emailMatch[0];
  if (urlMatch) next.website_url = normalizeUrl(urlMatch[0]);
  if (nameMatch && !/looking|trying|interested|ready|building|starting/i.test(nameMatch[1])) {
    next.name = nameMatch[1].trim();
  } else if (!next.name && stage === "intro_name" && looksLikeName(raw)) {
    next.name = raw.trim().replace(/[.!?]+$/g, "");
  }

  if (/\b(not sure|skip|skip for now|not now|i don't know|i do not know|unsure)\b/i.test(lower)) {
    next.strategy_question_skipped = true;
  }
  if ((stage === "concept_direction" || (currentFields?.email && !currentFields?.post_email_strategy_answer)) && !emailMatch) {
    next.post_email_strategy_answer = /\b(not sure|skip|skip for now|not now|i don't know|i do not know|unsure)\b/i.test(lower)
      ? "Skipped or not sure"
      : raw;
  }
  if ((stage === "concept_followup" || (currentFields?.post_email_strategy_answer && !currentFields?.post_email_followup_answer)) && !emailMatch) {
    next.post_email_followup_answer = /\b(not sure|skip|skip for now|not now|i don't know|i do not know|unsure)\b/i.test(lower)
      ? "Skipped or not sure"
      : raw;
  }
  return next;
}

function isAuditPending(status) {
  return ["PENDING", "STARTED", "PROGRESS", "RETRY", "RETRYING"].includes(String(status || "").toUpperCase());
}

function pricingContextFromEstimate(e) {
  if (!e) return null;
  return {
    pricing_version: "siteworks_founder_v2",
    standard_pages: Number(e.pages || 1),
    extra_standard_pages: Math.max(0, Number(e.pages || 1) - 5),
    landing_pages: Number(e.landingCount || 0),
    payment_preference: e.plan === "annual" ? "annual_upfront" : "monthly_12_month_minimum",
    setup_fee: Number(e.setupFee || 0),
    monthly_fee: 199,
    annual_discount_rate: 0.1,
    annual_savings: Number(e.annualSavings || 0),
    due_to_start: e.plan === "annual" ? Number(e.annualFY || 0) : Number(e.setupFee || 0),
    first_year_total: e.plan === "annual" ? Number(e.annualFY || 0) : Number(e.monthlyFY || 0),
    estimate_summary: estimateTranscript(e)
  };
}

function estimateRows(e) {
  const landingText = e.landingCount
    ? `${e.landingCount} campaign landing page${e.landingCount > 1 ? "s" : ""}`
    : "No campaign landing pages";
  const paymentText = e.plan === "annual" ? "Year-one care upfront" : "Website + monthly Site Care";
  return [
    ["Website scope", `${e.pages} page${e.pages > 1 ? "s" : ""}`],
    ["Payment path", paymentText],
    ["Landing pages", landingText],
    ["Website cost", BCN_USD(e.setupFee)],
    [e.plan === "annual" ? "Due upfront" : "Starting payment", BCN_USD(e.plan === "annual" ? e.annualFY : e.setupFee)],
    ["Monthly Site Care & Updates", e.plan === "annual" ? "Included for the year" : "$199/mo"],
    ["First-year total", BCN_USD(e.plan === "annual" ? e.annualFY : e.monthlyFY)],
    [e.plan === "annual" ? "Upfront payment savings" : "Upfront savings available", BCN_USD(e.annualSavings)]
  ];
}

function estimateTranscript(e) {
  return [
    `Scope: ${e.pages} standard page${e.pages === 1 ? "" : "s"}, ${e.landingCount ? `${e.landingCount} campaign landing page${e.landingCount > 1 ? "s" : ""}` : "no campaign landing pages"}.`,
    `Website cost: ${BCN_USD(e.setupFee)}`,
    `Due upfront: ${BCN_USD(e.plan === "annual" ? e.annualFY : e.setupFee)}`,
    `Monthly Site Care: ${e.plan === "annual" ? "Included for the first year" : "$199/month"}`,
    `First-year total: ${BCN_USD(e.plan === "annual" ? e.annualFY : e.monthlyFY)}`,
    `Annual savings: ${BCN_USD(e.annualSavings)}`
  ].join("\n");
}

function initialBeaconMessage() {
  return {
    id: uid(),
    role: "assistant",
    content: "Hi, I'm Beacon. I can help you figure out what your site needs to explain, prove, and make easier to do.\n\nShare your current website and I'll help shape the first homepage direction.\n\nWhat's your current website URL?"
  };
}

function compactMessages(messages) {
  return (messages || []).slice(-18).map((message) => ({
    role: message.role === "assistant" ? "assistant" : "user",
    content: String(message.transcriptContent || message.content || "").slice(0, 3000)
  })).filter((message) => message.content);
}

function normalizeAttachment(fileLike) {
  return {
    id: fileLike.id || uid("file"),
    name: String(fileLike.name || "Attachment").slice(0, 180),
    type: String(fileLike.type || "unknown").slice(0, 120),
    size: Number(fileLike.size || 0),
    content_preview: String(fileLike.content_preview || "").slice(0, 8000)
  };
}

function isReadableAttachment(file) {
  return /text|json|csv|markdown|xml/i.test(file.type || "") || /\.(txt|md|csv|json|rtf)$/i.test(file.name || "");
}

async function readAttachment(file) {
  const attachment = normalizeAttachment(file);
  if (isReadableAttachment(file) && file.size <= 120000) {
    try {
      attachment.content_preview = (await file.text()).slice(0, 8000);
    } catch (error) {
      attachment.content_preview = "";
    }
  }
  return attachment;
}

function chooseBySeed(items, seed) {
  if (!items.length) return "";
  return items[Math.abs(Number(seed || 0)) % items.length];
}

function thinkingStatus(fields, auditState, stage, messages = []) {
  const seed = messages.length + Math.round(Date.now() / 1000);
  if (fields?.email && fields?.post_email_strategy_answer) {
    return chooseBySeed([
      "Turning that into a sharper homepage angle.",
      "Preparing one more useful strategy prompt.",
      "Connecting your notes into a clearer direction."
    ], seed);
  }
  if (fields?.email && fields?.website_url) {
    return chooseBySeed([
      "Saving the thread and shaping the next question.",
      "Keeping your context together.",
      "Organizing the prototype notes."
    ], seed);
  }
  if (stage === "intro_name" || (fields?.website_url && !fields?.name)) {
    return chooseBySeed([
      "Reading the site context.",
      "Checking the URL and preparing the next step.",
      "Starting the site read in the background."
    ], seed);
  }
  if (fields?.website_url && isAuditPending(auditState?.status)) {
    return chooseBySeed([
      "Checking site signals in the background.",
      "Reading early homepage signals.",
      "Looking for useful context, not a final verdict."
    ], seed);
  }
  if (fields?.website_url && !auditState?.summary) {
    return chooseBySeed([
      "Preparing a useful next question.",
      "Turning the site context into a next step.",
      "Thinking through the homepage opportunity."
    ], seed);
  }
  return chooseBySeed(BEACON_THINK_LABELS, seed);
}

function localFallbackReply(text, fields, stage, pricingContext) {
  const lower = String(text || "").toLowerCase();
  if (/price|pricing|cost|monthly|upfront|annual|year|site care|included/.test(lower)) {
    const pricing = pricingContext
      ? `For the estimate attached here:\n\n${pricingContext.estimate_summary}\n\nThe monthly path keeps the starting payment lower. The year-one path applies the 10% first-year savings and includes Site Care for the first year. Final approval still depends on project fit and complexity.`
      : "Siteworks starts with a website cost plus Site Care. A focused one-page site starts at $1,000, five core pages are $1,500, pages after five add $250 each, campaign landing pages add $800 each, and Site Care is $199/month.";
    return `${pricing}\n\n${nextLocalQuestion(fields, stage)}`;
  }
  if (/include|process|timeline|how does|seo|aeo|hosting|maintenance|change request|ecommerce|booking|membership/.test(lower)) {
    return `Siteworks includes page structure, message clarity, visual credibility, responsive build, launch prep, AEO/SEO foundation, hosting, SSL, basic maintenance, and one light monthly change request through Site Care.\n\n${nextLocalQuestion(fields, stage)}`;
  }
  return nextLocalQuestion(fields, stage);
}

function nextLocalQuestion(fields, stage) {
  if (!fields?.website_url && !fields?.business_type) return "What's your current website URL? If you do not have one yet, tell me the business name and what the website should help people do.";
  if (fields.website_url && !fields.name) return "Got it. What should I call you?";
  if (!fields.current_pain && !fields.goal && !fields.primary_goal && !fields.strategy_question_skipped) return "One useful thing to know: what do you wish the site helped you get more of: calls, bookings, quote requests, consultations, store visits, or something else? If you are not sure yet, you can just say that.";
  if (!fields.email) return "I can save this so we don't lose the thread. What's the best email to keep these prototype notes tied to?";
  if (!fields.post_email_strategy_answer) return "Perfect, I can save this thread. One thing that will make the homepage prototype sharper: what should someone believe in the first 10 seconds before they trust you enough to reach out? If you are not sure, you can say that too.";
  if (!fields.post_email_followup_answer) return "That helps. One more useful angle before the call: what proof should show up earlier on the page, like reviews, credentials, project examples, years in business, or something else? If you are not sure, you can skip this too.";
  return "Okay, that gives us enough to start shaping the first homepage prototype. The clean next step is a call with Eduardo, our co-founder and website conversion expert.";
}

function leadStatusLabel(status) {
  return {
    exploring: "Exploring",
    qualified: "Fit forming",
    email_requested: "Concept ready",
    ready_to_book: "Ready to book",
    review_required: "Review needed",
    not_fit: "Not a fit"
  }[status] || "Exploring";
}

function BeaconChatProvider({ estimate, children }) {
  const savedSessionRef = React.useRef(loadBeaconSession());
  const savedSession = savedSessionRef.current || {};
  const [messages, setMessages] = React.useState(() => savedSession.messages || [initialBeaconMessage()]);
  const [thinking, setThinking] = React.useState(false);
  const [thinkingLabel, setThinkingLabel] = React.useState(BEACON_THINK_LABELS[0]);
  const [frame, setFrame] = React.useState(0);
  const [guestInitial, setGuestInitial] = React.useState(() => savedSession.guestInitial || (savedSession.collectedFields?.name ? savedSession.collectedFields.name[0].toUpperCase() : "G"));
  const [fullscreen, setFullscreen] = React.useState(false);
  const [sessionId, setSessionId] = React.useState(() => savedSession.sessionId || createSessionId());
  const [notionPageId, setNotionPageId] = React.useState(savedSession.notionPageId || "");
  const [entrySource, setEntrySource] = React.useState(savedSession.entrySource || "direct_chat");
  const [stage, setStage] = React.useState(savedSession.stage || "context_source");
  const [leadStatus, setLeadStatus] = React.useState(savedSession.leadStatus || "exploring");
  const [pricingContext, setPricingContext] = React.useState(savedSession.pricingContext || null);
  const [collectedFields, setCollectedFields] = React.useState(savedSession.collectedFields || {});
  const [attachments, setAttachments] = React.useState(savedSession.attachments || []);
  const [pendingAttachments, setPendingAttachments] = React.useState([]);
  const [quickReplies, setQuickReplies] = React.useState(savedSession.quickReplies || []);
  const [handoffActions, setHandoffActions] = React.useState(savedSession.handoffActions || []);
  const [handoffDocked, setHandoffDocked] = React.useState(Boolean(savedSession.handoffDocked));
  const [siteContext, setSiteContext] = React.useState(savedSession.siteContext || null);
  const [auditState, setAuditState] = React.useState(savedSession.auditState || { url: "", taskId: "", status: "", summary: null });
  const [bookingUrl, setBookingUrl] = React.useState(savedSession.bookingUrl || BEACON_CONFIG.bookingUrl);
  const [chatStatus, setChatStatus] = React.useState(savedSession.chatStatus || "");
  const [remoteUnavailable, setRemoteUnavailable] = React.useState(false);
  const [streaming, setStreaming] = React.useState(false);
  const streamTokenRef = React.useRef("");
  const lastEstimateRef = React.useRef("");
  const openFullscreen = React.useCallback(() => setFullscreen(true), []);
  const closeFullscreen = React.useCallback(() => setFullscreen(false), []);

  React.useEffect(() => {
    if (!thinking) return undefined;
    const id = window.setInterval(() => setFrame((f) => (f + 1) % BEACON_FRAMES.length), 720);
    return () => window.clearInterval(id);
  }, [thinking]);

  React.useEffect(() => {
    const openBeacon = () => openFullscreen();
    window.__beaconOpen = openBeacon;
    window.addEventListener("siteworks:open-beacon", openBeacon);
    return () => {
      window.removeEventListener("siteworks:open-beacon", openBeacon);
      if (window.__beaconOpen === openBeacon) delete window.__beaconOpen;
    };
  }, [openFullscreen]);

  React.useEffect(() => {
    if (streaming) return;
    saveBeaconSession({
      sessionId,
      notionPageId,
      entrySource,
      stage,
      leadStatus,
      pricingContext,
      collectedFields,
      attachments,
      quickReplies,
      handoffActions,
      handoffDocked,
      siteContext,
      auditState,
      bookingUrl,
      chatStatus,
      guestInitial,
      messages
    });
  }, [
    messages,
    sessionId,
    notionPageId,
    entrySource,
    stage,
    leadStatus,
    pricingContext,
    collectedFields,
    attachments,
    quickReplies,
    handoffActions,
    handoffDocked,
    siteContext,
    auditState,
    bookingUrl,
    chatStatus,
    guestInitial,
    streaming
  ]);

  React.useEffect(() => {
    if (!isAuditPending(auditState.status) || !auditState.taskId || remoteUnavailable) return undefined;
    const id = window.setTimeout(() => syncRemoteState("audit_poll"), 7000);
    return () => window.clearTimeout(id);
  }, [auditState.taskId, auditState.status, remoteUnavailable]);

  React.useEffect(() => {
    if (!estimate) return;
    const key = JSON.stringify({
      plan: estimate.plan,
      pages: estimate.pages,
      landingCount: estimate.landingCount,
      setupFee: estimate.setupFee,
      annualFY: estimate.annualFY,
      monthlyFY: estimate.monthlyFY,
      selectedAt: estimate.selectedAt
    });
    if (lastEstimateRef.current === key) return;
    lastEstimateRef.current = key;
    startPricingHandoff(estimate);
  }, [estimate]);

  function buildPayload(eventType, overrides = {}) {
    const fields = overrides.collectedFields || collectedFields;
    const audit = overrides.auditState || auditState;
    return {
      session_id: overrides.sessionId || sessionId,
      notion_page_id: overrides.notionPageId !== undefined ? overrides.notionPageId : notionPageId,
      event_type: eventType,
      entry_source: overrides.entrySource || entrySource,
      stage: overrides.stage || stage,
      pricing_context: overrides.pricingContext !== undefined ? overrides.pricingContext : pricingContext,
      messages: compactMessages(overrides.messages || messages),
      collected_fields: fields,
      attachments: (overrides.attachments !== undefined ? overrides.attachments : [...attachments, ...pendingAttachments]).map(normalizeAttachment).slice(-10),
      site_context: overrides.siteContext !== undefined ? overrides.siteContext : siteContext,
      audit_url: audit.url || fields.website_url || "",
      audit_task_id: audit.taskId || "",
      audit_status: audit.status || "",
      audit_summary: audit.summary || null
    };
  }

  async function requestRemoteReply(eventType = "message", overrides = {}) {
    if (!BEACON_CONFIG.endpoint || !BEACON_CONFIG.enableRemote || (remoteUnavailable && !overrides.forceRemote)) return null;
    const response = await fetch(BEACON_CONFIG.endpoint, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(buildPayload(eventType, overrides))
    });
    if (!response.ok) throw new Error(`Beacon endpoint failed with ${response.status}`);
    return response.json();
  }

  function applyRemoteState(remoteReply) {
    if (!remoteReply) return;
    if (remoteReply.collected_fields) {
      setCollectedFields((current) => {
        const next = { ...current, ...remoteReply.collected_fields };
        if (next.name) setGuestInitial(guestInitialFrom(next.name) || next.name[0].toUpperCase());
        return next;
      });
    }
    if (remoteReply.stage) setStage(remoteReply.stage);
    if (remoteReply.lead_status) setLeadStatus(remoteReply.lead_status);
    if (remoteReply.pricing_context) setPricingContext(remoteReply.pricing_context);
    if (remoteReply.notion_page_id) setNotionPageId(remoteReply.notion_page_id);
    if (remoteReply.site_context) setSiteContext(remoteReply.site_context);
    if (remoteReply.booking_url) setBookingUrl(remoteReply.booking_url || BEACON_CONFIG.bookingUrl);
    if (Array.isArray(remoteReply.quick_replies)) setQuickReplies(remoteReply.quick_replies);
    if (Array.isArray(remoteReply.handoff_actions) && remoteReply.handoff_actions.length) {
      setHandoffActions((current) => {
        if (remoteReply.handoff_actions.length && !current.length) setHandoffDocked(false);
        return remoteReply.handoff_actions;
      });
    }
    if (remoteReply.audit_task_id !== undefined || remoteReply.audit_status !== undefined || remoteReply.audit_summary) {
      setAuditState((current) => ({
        url: remoteReply.collected_fields?.website_url || collectedFields.website_url || current.url || "",
        taskId: remoteReply.audit_task_id || current.taskId || "",
        status: remoteReply.audit_status || current.status || "",
        summary: remoteReply.audit_summary || current.summary || null
      }));
    }
  }

  async function syncRemoteState(eventType, overrides = {}) {
    try {
      const remoteReply = await requestRemoteReply(eventType, overrides);
      applyRemoteState(remoteReply);
      return remoteReply;
    } catch (error) {
      setRemoteUnavailable(true);
      return null;
    }
  }

  function setHandoffParts(messageId, updater) {
    setMessages((current) => current.map((message) => {
      if (message.id !== messageId) return message;
      return { ...message, parts: updater(message.parts || []) };
    }));
  }

  async function streamParagraph(messageId, text, token, speed = 4) {
    const partId = uid("part");
    setHandoffParts(messageId, (parts) => [...parts, { id: partId, kind: "p", text: "", typing: true }]);
    let shown = "";
    for (const char of text) {
      if (streamTokenRef.current !== token) return false;
      shown += char;
      setHandoffParts(messageId, (parts) => parts.map((part) => part.id === partId ? { ...part, text: shown } : part));
      await wait(/[.?]/.test(char) ? speed * 5 : speed);
    }
    setHandoffParts(messageId, (parts) => parts.map((part) => part.id === partId ? { ...part, typing: false } : part));
    return true;
  }

  async function streamEstimateCard(messageId, rows, token) {
    const partId = uid("estimate");
    setHandoffParts(messageId, (parts) => [...parts, { id: partId, kind: "estimate", rows: [] }]);
    for (const row of rows) {
      if (streamTokenRef.current !== token) return false;
      setHandoffParts(messageId, (parts) => parts.map((part) => (
        part.id === partId ? { ...part, rows: [...part.rows, row] } : part
      )));
      await wait(85);
    }
    return true;
  }

  async function startPricingHandoff(nextEstimate) {
    const token = uid("stream");
    const nextSessionId = createSessionId(true);
    const nextPricingContext = pricingContextFromEstimate(nextEstimate);
    const messageId = uid("handoff");
    const transcriptContent = `Hi, I'm Beacon. I can help you figure out what your site needs to explain, prove, and make easier to do.\n\nBased on what you filled out in the pricing calculator, you are looking at:\n\n${estimateTranscript(nextEstimate)}\n\nThe useful next step is simple: share your current site, answer a few questions, and a website conversion expert can use that to shape a private homepage prototype around your business.\n\nWhat's your current website URL?`;
    const handoffMessage = {
      id: messageId,
      role: "assistant",
      kind: "pricing_handoff",
      content: transcriptContent,
      parts: []
    };

    streamTokenRef.current = token;
    setStreaming(true);
    setThinking(false);
    setFrame(0);
    setSessionId(nextSessionId);
    setEntrySource("pricing_calculator");
    setStage("context_source");
    setLeadStatus("exploring");
    setPricingContext(nextPricingContext);
    setCollectedFields({});
    setAttachments([]);
    setPendingAttachments([]);
    setQuickReplies([]);
    setHandoffActions([]);
    setHandoffDocked(false);
    setSiteContext(null);
    setAuditState({ url: "", taskId: "", status: "", summary: null });
    setNotionPageId("");
    setRemoteUnavailable(false);
    setChatStatus("Estimate attached. Beacon is laying out the context.");
    setMessages([handoffMessage]);

    await wait(350);
    if (!await streamParagraph(messageId, "Hi, I'm Beacon. I can help you figure out what your site needs to explain, prove, and make easier to do.", token)) return;
    if (!await streamParagraph(messageId, "Based on what you filled out in the pricing calculator, you are looking at:", token)) return;
    if (!await streamEstimateCard(messageId, estimateRows(nextEstimate), token)) return;
    if (!await streamParagraph(messageId, "The useful next step is simple: share your current site, answer a few questions, and a website conversion expert can use that to shape a private homepage prototype around your business.", token)) return;
    if (!await streamParagraph(messageId, "What's your current website URL?", token)) return;

    setStreaming(false);
    setChatStatus("Estimate attached. The next replies will use this pricing context.");
    syncRemoteState("pricing_attached", {
      sessionId: nextSessionId,
      entrySource: "pricing_calculator",
      stage: "context_source",
      pricingContext: nextPricingContext,
      messages: [handoffMessage],
      collectedFields: {},
      attachments: [],
      auditState: { url: "", taskId: "", status: "", summary: null },
      forceRemote: true
    });
  }

  async function addAttachmentFiles(files) {
    const currentCount = pendingAttachments.length;
    const nextFiles = Array.from(files || []).slice(0, Math.max(0, 5 - currentCount));
    if (!nextFiles.length) return;
    const readFiles = await Promise.all(nextFiles.map(readAttachment));
    setPendingAttachments((current) => [...current, ...readFiles]);
    setChatStatus(`${readFiles.length} document${readFiles.length === 1 ? "" : "s"} attached. Send your message when ready.`);
  }

  function removePendingAttachment(id) {
    setPendingAttachments((current) => current.filter((file) => file.id !== id));
  }

  async function send(text, options = {}) {
    const raw = String(text || "").trim();
    const filesToSend = pendingAttachments.slice();
    if ((!raw && !filesToSend.length) || thinking || streaming) return;

    const displayText = options.displayText || raw || "Attached document for context.";
    const transcriptText = raw || displayText;
    const nextFields = applyLocalLeadFields(transcriptText, collectedFields, stage);
    const initial = guestInitialFrom(transcriptText) || (nextFields.name ? guestInitialFrom(nextFields.name) : null);
    const nextAttachments = [...attachments, ...filesToSend];
    const userMessage = {
      id: uid(),
      role: "user",
      content: displayText,
      transcriptContent: transcriptText,
      attachments: filesToSend
    };
    const nextMessages = [...messages, userMessage];

    if (initial) setGuestInitial(initial);
    if (handoffActions.length) setHandoffDocked(true);
    setMessages(nextMessages);
    setPendingAttachments([]);
    setAttachments(nextAttachments);
    setCollectedFields(nextFields);
    setQuickReplies([]);
    setThinking(true);
    setFrame(0);
    const nextThinkingStatus = thinkingStatus(nextFields, auditState, stage, nextMessages);
    setThinkingLabel(nextThinkingStatus);
    setChatStatus(nextThinkingStatus);

    try {
      const remoteReply = await requestRemoteReply("message", {
        messages: nextMessages,
        collectedFields: nextFields,
        attachments: nextAttachments
      });
      await wait(420);
      setThinking(false);
      if (remoteReply) {
        applyRemoteState(remoteReply);
        if (!remoteReply.silent) {
          setMessages((current) => [...current, {
            id: uid(),
            role: "assistant",
            content: remoteReply.message || localFallbackReply(transcriptText, nextFields, stage, pricingContext)
          }]);
        }
        setChatStatus(remoteReply.source === "fallback" ? "Beacon is connected, using guided fallback while the LLM response is unavailable." : "");
        return;
      }
    } catch (error) {
      setRemoteUnavailable(true);
    }

    await wait(420);
    setThinking(false);
    setMessages((current) => [...current, {
      id: uid(),
      role: "assistant",
      content: localFallbackReply(transcriptText, nextFields, stage, pricingContext)
    }]);
    setChatStatus(BEACON_CONFIG.enableRemote ? "Using guided fallback while the secure Beacon endpoint is unavailable." : "Prototype mode: open through the local server to use live Beacon responses.");
  }

  function bookCall() {
    window.location.href = bookingUrl || "https://calendly.com/taurist/siteworks";
  }

  const value = {
    messages,
    thinking,
    thinkingLabel,
    frame,
    guestInitial,
    fullscreen,
    estimate,
    send,
    addAttachmentFiles,
    removePendingAttachment,
    pendingAttachments,
    quickReplies,
    handoffActions,
    handoffDocked,
    bookCall,
    chatStatus,
    leadStatus,
    leadStatusLabel: leadStatusLabel(leadStatus),
    streaming,
    started: messages.some((message) => message.role === "user"),
    openFullscreen,
    closeFullscreen,
  };

  return <BeaconChatContext.Provider value={value}>{children}</BeaconChatContext.Provider>;
}

function BeaconFace({ sm }) {
  return (
    <span className={"beacon-av " + (sm ? "beacon-av--sm" : "beacon-av--head")} aria-hidden="true">
      <img src="assets/beacon-face.png" alt="" />
    </span>
  );
}

function BeaconThinking({ frame, label }) {
  return (
    <div className="chat-msg chat-msg--sol beacon-think">
      <span className="beacon-av beacon-av--sm" aria-hidden="true">
        <span className="beacon-think-frames">
          {BEACON_FRAMES.map((src, i) => (
            <img key={i} src={src} alt="" className={i === frame ? "is-on" : ""} />
          ))}
        </span>
      </span>
      <div className="beacon-think-body">
        <div className="beacon-think-label">{label || BEACON_THINK_LABELS[frame]}</div>
        <div className="beacon-dots" role="status" aria-label="Beacon is preparing an answer">
          {[0, 1, 2, 3].map((i) =>
            i === 3 && frame >= 3
              ? <i key={i} className="ri-check-line" />
              : <span key={i} className={i <= frame ? "on" : ""} />
          )}
        </div>
      </div>
    </div>
  );
}

function InlineText({ text }) {
  return (
    <React.Fragment>
      {String(text || "").split(/(\*\*[^*]+\*\*)/g).map((part, i) => {
        if (/^\*\*[^*]+\*\*$/.test(part)) return <strong key={i}>{part.slice(2, -2)}</strong>;
        return <React.Fragment key={i}>{part}</React.Fragment>;
      })}
    </React.Fragment>
  );
}

function MessageText({ content }) {
  const blocks = [];
  let list = [];
  String(content || "").split(/\n/).forEach((line, index) => {
    const bullet = line.match(/^\s*[-*]\s+(.+)/);
    if (bullet) {
      list.push({ key: index, text: bullet[1] });
      return;
    }
    if (list.length) {
      blocks.push({ kind: "list", items: list });
      list = [];
    }
    if (line.trim()) blocks.push({ kind: "p", text: line.trim() });
  });
  if (list.length) blocks.push({ kind: "list", items: list });

  return (
    <React.Fragment>
      {blocks.map((block, index) => {
        if (block.kind === "list") {
          return <ul key={index}>{block.items.map((item) => <li key={item.key}><InlineText text={item.text} /></li>)}</ul>;
        }
        return <p key={index}><InlineText text={block.text} /></p>;
      })}
    </React.Fragment>
  );
}

function EstimateRowsCard({ rows }) {
  return (
    <div className="chat-estimate-card" data-hook="estimate-card">
      <div className="cec-title">Selected Siteworks direction</div>
      {rows.map(([label, value], index) => (
        <div className="cec-row stream-in" key={index}>
          <span>{label}</span>
          <b>{value}</b>
        </div>
      ))}
    </div>
  );
}

function PricingHandoffMessage({ message }) {
  return (
    <React.Fragment>
      {(message.parts || []).map((part) => {
        if (part.kind === "estimate") return <EstimateRowsCard key={part.id} rows={part.rows || []} />;
        return (
          <p key={part.id}>
            <InlineText text={part.text} />
            {part.typing && <span className="typing-caret" aria-hidden="true" />}
          </p>
        );
      })}
    </React.Fragment>
  );
}

function FileChip({ file, onRemove }) {
  return (
    <span className="file-chip">
      {file.name}{file.size ? ` (${Math.max(1, Math.round(file.size / 1024))} KB)` : ""}
      {onRemove && <button type="button" aria-label={`Remove ${file.name}`} onClick={() => onRemove(file.id)}>x</button>}
    </span>
  );
}

function BeaconQuickReplies() {
  const c = useBeaconChat();
  if (!c.quickReplies || !c.quickReplies.length || c.thinking || c.streaming) return null;
  return (
    <div className="chat-quick-actions" data-hook="quick-actions">
      {c.quickReplies.slice(0, 4).map((reply) => (
        <button type="button" key={reply.label} onClick={() => c.send(reply.prompt, { displayText: reply.label })}>
          {reply.label}
        </button>
      ))}
    </div>
  );
}

function BeaconHandoffActions({ placement = "inline" }) {
  const c = useBeaconChat();
  if (!c.handoffActions || !c.handoffActions.length) return null;
  if (placement === "inline" && c.handoffDocked) return null;
  if (placement === "docked" && !c.handoffDocked) return null;
  return (
    <div className={"chat-handoff-actions chat-handoff-actions--" + placement + " chat-booking-card"} data-hook="handoff-actions">
      {c.handoffActions.includes("book_concept_call") && (
        <React.Fragment>
          <div className="chat-booking-copy">
            <span className="chat-booking-kicker">Next step</span>
            <strong>Book a homepage prototype call with Eduardo</strong>
            <p>Review the homepage direction with our co-founder and website conversion expert.</p>
          </div>
          <button type="button" className="btn btn-primary chat-booking-btn" onClick={c.bookCall}>Book homepage prototype call <i className="ri-arrow-right-line" /></button>
        </React.Fragment>
      )}
    </div>
  );
}

function BeaconAttachmentTray() {
  const c = useBeaconChat();
  if (!c.pendingAttachments.length) return null;
  return (
    <div className="chat-attachment-tray" data-hook="attachment-tray">
      {c.pendingAttachments.map((file) => <FileChip key={file.id} file={file} onRemove={c.removePendingAttachment} />)}
    </div>
  );
}

function BeaconMessages() {
  const c = useBeaconChat();
  return (
    <React.Fragment>
      {c.messages.map((m) => {
        if (m.role === "user") {
          return (
            <div key={m.id} className="chat-msg chat-msg--you">
              <span className="chat-you-av" aria-hidden="true">{c.guestInitial}</span>
              <div className="chat-you-bubble">
                {m.content}
                {m.attachments && m.attachments.length > 0 && (
                  <div className="message-files">{m.attachments.map((file) => <FileChip key={file.id} file={file} />)}</div>
                )}
              </div>
            </div>
          );
        }
        return (
          <div key={m.id} className="chat-msg chat-msg--sol">
            <BeaconFace sm />
            <div className="chat-bubble">
              <span className="chat-sender">Beacon</span>
              {m.kind === "pricing_handoff" ? <PricingHandoffMessage message={m} /> : <MessageText content={m.content} />}
            </div>
          </div>
        );
      })}
      {c.thinking && <BeaconThinking frame={c.frame} label={c.thinkingLabel} />}
      <BeaconQuickReplies />
      <BeaconHandoffActions placement="inline" />
    </React.Fragment>
  );
}

function BeaconComposer({ onSend, autoFocus }) {
  const c = useBeaconChat();
  const [draft, setDraft] = React.useState("");
  const inputRef = React.useRef(null);
  const fileRef = React.useRef(null);
  const returnFocusRef = React.useRef(false);

  React.useEffect(() => {
    if (autoFocus && inputRef.current) inputRef.current.focus();
  }, [autoFocus]);

  React.useEffect(() => {
    if (!returnFocusRef.current || c.thinking || c.streaming) return;
    const id = window.setTimeout(() => {
      if (inputRef.current) inputRef.current.focus();
    }, 40);
    return () => window.clearTimeout(id);
  }, [c.thinking, c.streaming, c.messages.length]);

  function submit() {
    const value = draft.trim();
    if (!value && !c.pendingAttachments.length) return;
    returnFocusRef.current = true;
    onSend(value);
    setDraft("");
  }

  return (
    <React.Fragment>
      <BeaconAttachmentTray />
      <div className="chat-input" data-hook="chat-input">
        <input ref={fileRef} type="file" multiple hidden onChange={async (event) => {
          await c.addAttachmentFiles(event.target.files);
          event.target.value = "";
          if (inputRef.current) inputRef.current.focus();
        }} />
        <button type="button" className="chat-attach" aria-label="Add attachment" onClick={() => fileRef.current && fileRef.current.click()}>
          <i className="ri-add-line" />
        </button>
        <input
          ref={inputRef}
          type="text"
          className="chat-field"
          data-hook="chat-message-field"
          placeholder="Message Beacon..."
          aria-label="Message Beacon"
          value={draft}
          disabled={c.thinking || c.streaming}
          onFocus={() => { returnFocusRef.current = true; }}
          onChange={(ev) => {
            returnFocusRef.current = true;
            setDraft(ev.target.value);
          }}
          onKeyDown={(ev) => {
            if (ev.key === "Enter") {
              ev.preventDefault();
              submit();
            }
          }}
        />
        <button type="button" className="chat-send" aria-label="Send message" onClick={submit} disabled={(!draft.trim() && !c.pendingAttachments.length) || c.thinking || c.streaming}>
          <i className="ri-arrow-up-line" />
        </button>
      </div>
      <BeaconHandoffActions placement="docked" />
    </React.Fragment>
  );
}

function BeaconFullscreen() {
  const c = useBeaconChat();
  const threadRef = React.useRef(null);

  React.useEffect(() => {
    const t = threadRef.current;
    if (t) t.scrollTop = t.scrollHeight;
  }, [c.messages, c.thinking, c.frame, c.fullscreen, c.quickReplies, c.handoffActions]);

  React.useEffect(() => {
    if (!c.fullscreen) return undefined;
    function onKey(e) {
      if (e.key === "Escape") c.closeFullscreen();
    }
    window.addEventListener("keydown", onKey);
    document.body.style.overflow = "hidden";
    return () => {
      window.removeEventListener("keydown", onKey);
      document.body.style.overflow = "";
    };
  }, [c.fullscreen]);

  if (!c.fullscreen) return null;
  return (
    <div className="bcf-overlay" role="dialog" aria-modal="true" aria-label="Beacon chat">
      <div className="bcf-backdrop" onClick={c.closeFullscreen} />
      <div className="bcf-panel">
        <div className="bcf-head">
          <span className="beacon-av beacon-av--head" aria-hidden="true"><img src="assets/beacon-face.png" alt="Beacon" /></span>
          <div className="bcf-head-text">
            <h4 className="chat-title">Beacon</h4>
            <p className="chat-subtitle">{c.leadStatusLabel}</p>
          </div>
          <span className="pay-badge pay-badge--violet" style={{ flexShrink: 0 }}><span className="pay-badge-dot" />{c.leadStatusLabel}</span>
          <button type="button" className="chat-expand bcf-collapse" aria-label="Exit full screen" onClick={c.closeFullscreen}><i className="ri-fullscreen-exit-line" /></button>
          <button type="button" className="chat-expand" aria-label="Close chat" onClick={c.closeFullscreen}><i className="ri-close-line" /></button>
        </div>
        <div className="bcf-thread" ref={threadRef}>
          <div className="bcf-thread-inner"><BeaconMessages /></div>
        </div>
        <div className="bcf-foot">
          {c.chatStatus && <p className="chat-status">{c.chatStatus}</p>}
          <BeaconComposer onSend={c.send} autoFocus />
        </div>
      </div>
    </div>
  );
}

Object.assign(window, {
  BeaconChatProvider,
  useBeaconChat,
  BeaconMessages,
  BeaconThinking,
  BeaconComposer,
  BeaconFace,
  BeaconFullscreen,
  guestInitialFrom,
});
