// ============ SETTINGS TAB ============
// Six sub-panes in a left rail. The "Model" pane reads PROVIDERS and
// shows .env status (see /.env.example for the keys it expects).

// Industries — flat list. Keep it tight (~24) so the dropdown stays usable.
const INDUSTRIES = [
  "Restaurant · catering",
  "Café · bakery",
  "Retail · store",
  "E-commerce · DTC",
  "Wholesale · distribution",
  "Professional services",
  "Marketing · agency",
  "Media · publishing",
  "Technology · software",
  "Finance · banking",
  "Insurance",
  "Legal · law firm",
  "Healthcare · clinic",
  "Wellness · fitness",
  "Beauty · personal care",
  "Education · training",
  "Real estate · property",
  "Construction · trades",
  "Manufacturing",
  "Logistics · transportation",
  "Travel · hospitality",
  "Energy · utilities",
  "Non-profit · charity",
  "Government · public sector",
  "Other",
];

// Time zones — friendly labels with UTC offsets, grouped by region.
// Stored value matches the label so existing free-text "Europe / London"
// values keep working unchanged.
const TIMEZONE_GROUPS = [
  ["Americas", [
    "Pacific (Los Angeles · UTC−8)",
    "Mountain (Denver · UTC−7)",
    "Central (Chicago · UTC−6)",
    "Eastern (New York · UTC−5)",
    "Atlantic (Halifax · UTC−4)",
    "Mexico City · UTC−6",
    "Bogotá · UTC−5",
    "Lima · UTC−5",
    "Santiago · UTC−4",
    "São Paulo · UTC−3",
    "Buenos Aires · UTC−3",
  ]],
  ["Europe", [
    "Reykjavík · UTC+0",
    "London · UTC+0/+1",
    "Lisbon · UTC+0/+1",
    "Paris · UTC+1/+2",
    "Berlin · UTC+1/+2",
    "Madrid · UTC+1/+2",
    "Rome · UTC+1/+2",
    "Amsterdam · UTC+1/+2",
    "Copenhagen · UTC+1/+2",
    "Athens · UTC+2/+3",
    "Helsinki · UTC+2/+3",
    "Istanbul · UTC+3",
    "Moscow · UTC+3",
  ]],
  ["Africa", [
    "Casablanca · UTC+1",
    "Lagos · UTC+1",
    "Cairo · UTC+2",
    "Johannesburg · UTC+2",
    "Nairobi · UTC+3",
  ]],
  ["Middle East", [
    "Tel Aviv · UTC+2/+3",
    "Riyadh · UTC+3",
    "Tehran · UTC+3:30",
    "Dubai · UTC+4",
  ]],
  ["Asia", [
    "Karachi · UTC+5",
    "Mumbai · UTC+5:30",
    "Dhaka · UTC+6",
    "Bangkok · UTC+7",
    "Jakarta · UTC+7",
    "Singapore · UTC+8",
    "Hong Kong · UTC+8",
    "Shanghai · UTC+8",
    "Seoul · UTC+9",
    "Tokyo · UTC+9",
  ]],
  ["Oceania", [
    "Perth · UTC+8",
    "Sydney · UTC+10/+11",
    "Auckland · UTC+12/+13",
  ]],
];
const ALL_TIMEZONES = TIMEZONE_GROUPS.flatMap(([, list]) => list);
// Legacy values from earlier saves should still display; map them to the
// closest new label so the dropdown shows something selected.
const LEGACY_TZ_MAP = {
  "Europe / London": "London · UTC+0/+1",
};
function normalizeTz(v) {
  if (!v) return ALL_TIMEZONES[0];
  if (LEGACY_TZ_MAP[v]) return LEGACY_TZ_MAP[v];
  return v;
}

// ============ AGENT ARCHETYPES + INDUSTRY RECOMMENDATIONS ============
// Each archetype carries the architect's flow blocks (when/do/fallback)
// so clicking "Hire" can drop the user straight into a pre-filled
// architect modal. The recommendation engine ranks archetypes by industry
// and refines them with keywords from the user's "what we do" description.
const AGENT_ARCHETYPES = {
  "wa-helper":   { name: "WhatsApp Helper",      role: "Customer support",    icon: "💬", color: "#25D366", flow: { when: "wa-msg",   do: ["answer"],          fallback: "ping-phone" } },
  "booker":      { name: "Booking Concierge",    role: "Front-of-house",      icon: "🪑", color: "#E85D1A", flow: { when: "wa-msg",   do: ["book", "tag"],     fallback: "ping-phone" } },
  "lead-grab":   { name: "Lead Grabber",         role: "Sales · forms",       icon: "✦",  color: "#3B7CFF", flow: { when: "form-sub", do: ["lead", "reply"],   fallback: "ping-email" } },
  "follow-up":   { name: "Polite Follow-Up",     role: "Sales · email",       icon: "→",  color: "#3B7CFF", flow: { when: "lead-new", do: ["reply", "tag"],    fallback: "ping-email" } },
  "monday-an":   { name: "Monday Morning Analyst", role: "Finance · weekly",  icon: "📊", color: "#2DAF6B", flow: { when: "monday",   do: ["summary"],         fallback: "ping-email" } },
  "invoice-c":   { name: "Invoice Chaser",       role: "Finance · A/R",       icon: "💰", color: "#7A5BCF", flow: { when: "monday",   do: ["reply", "tag"],    fallback: "ping-email" } },
  "refund":      { name: "Refund Handler",       role: "Customer · billing",  icon: "↩",  color: "#C95A4F", flow: { when: "email-in", do: ["draft"],           fallback: "approval"   } },
  "review-rep":  { name: "Review Reply-er",      role: "Customer · reviews",  icon: "★",  color: "#D17C2A", flow: { when: "review",   do: ["reply"],           fallback: "ping-email" } },
  "contract":    { name: "Contract Reader",      role: "Legal · ad-hoc",      icon: "📄", color: "#B4488C", flow: { when: "forward",  do: ["draft"],           fallback: "ping-email" } },
  "calendar":    { name: "Calendar Host",        role: "Ops · scheduling",    icon: "📅", color: "#4285F4", flow: { when: "email-in", do: ["book"],            fallback: "ping-email" } },
  "intern":      { name: "Research Intern",      role: "Ops · ad-hoc",        icon: "🔍", color: "#5B8DBF", flow: { when: "forward",  do: ["summary"],         fallback: "ping-email" } },
  "standup":     { name: "Stand-up Summarizer",  role: "Ops · daily",         icon: "📋", color: "#3DA28A", flow: { when: "monday",   do: ["summary"],         fallback: "ping-email" } },
  "onboard":     { name: "Onboarding Buddy",     role: "Customer · success",  icon: "🎉", color: "#2DAF6B", flow: { when: "form-sub", do: ["reply"],           fallback: "ping-email" } },
  "doc-up":      { name: "Document Triage",      role: "Ops · documents",     icon: "🗂", color: "#7A5BCF", flow: { when: "doc-up",   do: ["summary", "tag"],  fallback: "ping-email" } },
  "intake":      { name: "Intake Agent",         role: "First contact",       icon: "▶",  color: "#3B7CFF", flow: { when: "form-sub", do: ["reply", "tag"],    fallback: "ping-email" } },
};

// Each industry → ordered archetype ids (best fit first). Anything not
// listed falls back to the "default" set.
const INDUSTRY_RECS = {
  "Restaurant · catering":     ["booker", "wa-helper", "review-rep", "calendar"],
  "Café · bakery":             ["wa-helper", "booker", "review-rep"],
  "Retail · store":            ["wa-helper", "review-rep", "lead-grab"],
  "E-commerce · DTC":          ["refund", "review-rep", "lead-grab", "wa-helper"],
  "Wholesale · distribution":  ["lead-grab", "follow-up", "invoice-c"],
  "Professional services":     ["intake", "calendar", "follow-up", "monday-an"],
  "Marketing · agency":        ["lead-grab", "intern", "standup", "review-rep"],
  "Media · publishing":        ["intern", "review-rep", "standup"],
  "Technology · software":     ["onboard", "intern", "standup", "monday-an"],
  "Finance · banking":         ["monday-an", "invoice-c", "contract", "intake"],
  "Insurance":                 ["intake", "doc-up", "follow-up", "monday-an"],
  "Legal · law firm":          ["contract", "intake", "calendar", "doc-up"],
  "Healthcare · clinic":       ["booker", "intake", "wa-helper", "calendar"],
  "Wellness · fitness":        ["booker", "wa-helper", "review-rep", "follow-up"],
  "Beauty · personal care":    ["booker", "wa-helper", "review-rep"],
  "Education · training":      ["intake", "onboard", "calendar", "follow-up"],
  "Real estate · property":    ["lead-grab", "calendar", "wa-helper", "follow-up"],
  "Construction · trades":     ["intake", "calendar", "invoice-c", "follow-up"],
  "Manufacturing":             ["lead-grab", "follow-up", "invoice-c", "intern"],
  "Logistics · transportation":["wa-helper", "intake", "follow-up"],
  "Travel · hospitality":      ["booker", "wa-helper", "review-rep", "calendar"],
  "Energy · utilities":        ["intake", "invoice-c", "monday-an"],
  "Non-profit · charity":      ["intake", "follow-up", "standup"],
  "Government · public sector":["intake", "doc-up", "calendar"],
  "Other":                     ["wa-helper", "lead-grab", "monday-an", "review-rep"],
};

// Keyword bumps from the description: words that nudge an archetype's
// rank up. e.g. "delivery" → wa-helper score+1; "subscription" → refund.
const KEYWORD_BUMPS = [
  [/\bbooking|reserv|table|appointment|appt\b/i, ["booker", "calendar"]],
  [/\bwhatsapp|wa\b/i,                            ["wa-helper", "booker"]],
  [/\bcontract|nda|legal|sign\b/i,                ["contract", "doc-up"]],
  [/\binvoice|receipt|payment|chase|overdue\b/i,  ["invoice-c", "refund"]],
  [/\brefund|return|chargeback\b/i,               ["refund"]],
  [/\breview|rating|yelp|google\b/i,              ["review-rep"]],
  [/\blead|prospect|inquir|inquiry|intake|form\b/i, ["lead-grab", "intake", "follow-up"]],
  [/\bcalendar|schedul|meeting|demo\b/i,          ["calendar", "follow-up"]],
  [/\breport|summary|weekly|monday|kpi|metric\b/i, ["monday-an", "standup"]],
  [/\bclient|customer|onboard|welcome\b/i,        ["onboard", "wa-helper"]],
  [/\bemail|gmail|inbox\b/i,                      ["intake", "follow-up"]],
  [/\bcatering|delivery|menu\b/i,                 ["booker", "wa-helper"]],
];

// Per-archetype "why" text — varies slightly by industry so it reads as
// tailored rather than generic. The user's description is not echoed
// verbatim (would feel uncanny) but its keywords influence which lines win.
function whyFor(id, industry) {
  const compact = (s) => s.replace(/^./, (c) => c.toUpperCase());
  const generic = {
    "wa-helper":  "Answers customers on WhatsApp, books, and sends you the leads.",
    "booker":     "Confirms bookings, checks availability, holds buffers — without back-and-forth.",
    "lead-grab":  "Captures every form fill, replies in your voice, and logs it for sales.",
    "follow-up":  "Three polite touches. Stops the second they reply.",
    "monday-an":  "Reads your books every Monday and writes a plain-English summary.",
    "invoice-c":  "Chases overdue invoices on a polite cadence. Pauses if you're already talking.",
    "refund":     "Drafts refund replies, flags the big ones for you to approve.",
    "review-rep": "Replies to every Google + Yelp review in your voice; flags negatives.",
    "contract":   "Reads any contract you forward and tells you, in plain English, where the weird stuff is.",
    "calendar":   "Schedules meetings without the back-and-forth. Knows your buffers and protected time.",
    "intern":     "Researches anything you ask — companies, people, markets — and gives you a one-pager.",
    "standup":    "Reads team standups, makes one short summary so you don't have to scroll.",
    "onboard":    "Welcomes new sign-ups, gets them to first value, and checks in at day 7.",
    "doc-up":     "Triages every document that lands — what it is, who it's for, what it needs.",
    "intake":     "First-contact agent for every inquiry — qualifies, captures, and routes.",
  };
  // Industry-tailored phrasing where it adds value.
  const tailored = {
    "Restaurant · catering": {
      "booker":    "Books tables and holds dietary requirements — never overbooks.",
      "wa-helper": "Handles \"do you have a table at 8?\" so you don't lose dinners to slow replies.",
    },
    "Healthcare · clinic": {
      "booker":    "Books appointments around your block schedule and confirms day-before.",
      "intake":    "Greets new patients, captures details, routes urgent cases.",
    },
    "Real estate · property": {
      "lead-grab": "Captures every viewing-request, replies with the next available slot.",
      "calendar":  "Books viewings — keeps drive-time buffers between properties.",
    },
    "Legal · law firm": {
      "contract":  "Reads incoming contracts before you do; flags non-standard clauses.",
      "intake":    "Greets every new matter, captures jurisdiction + conflict checks.",
    },
    "E-commerce · DTC": {
      "refund":    "Handles refund-and-return replies; escalates the big ones for sign-off.",
      "review-rep":"Keeps your Google + Trustpilot ratings clean — replies in your voice.",
    },
    "Marketing · agency": {
      "lead-grab": "Captures every \"contact us\" — qualifies, scores, and books the first call.",
      "standup":   "Summarizes every team standup so account leads don't have to scroll Slack.",
    },
  };
  const t = (tailored[industry] || {})[id];
  return t || compact(generic[id] || "Helps you get back hours every week.");
}

// ============ WEBSITE ANALYSIS ============
// We can't fetch arbitrary URLs from a static page (CORS), so we go
// through public CORS-relay services. Then we parse the HTML, extract
// meaningful chunks (title, descriptions, h1, hero, nav, buttons, body
// text), and run a unified signal library over the result. Each signal
// names a real capability on the site (booking, pricing, contact form,
// services, blog, reviews…) and votes for the agents that would help.
const BUSINESS_SIGNALS = [
  // ---- Strong capability signals (drive recommendations) ----
  { id: "booking",      label: "online booking",       evidence: /\b(book\s+(?:a\s+)?(?:table|appointment|reservation|consultation|call|slot|tour|viewing)|make\s+a\s+reservation|reserve\s+(?:a|your))\b/i, agents: ["booker", "calendar"] },
  { id: "appointments", label: "appointments",         evidence: /\b(appointments?|consult(?:ations?)?|first\s+visit|new\s+patient|book\s+now)\b/i, agents: ["booker", "intake", "calendar"] },
  { id: "ecommerce",    label: "online store",         evidence: /\b(add\s+to\s+(?:cart|bag|basket)|checkout|order\s+(?:now|online)|buy\s+now|free\s+shipping|free\s+delivery|in\s+stock|out\s+of\s+stock)\b/i, agents: ["refund", "review-rep", "wa-helper"], industry: "E-commerce · DTC" },
  { id: "pricing",      label: "pricing tiers",        evidence: /\b(pricing|plans?\b|free\s+trial|per\s+(?:month|seat|user)|\/mo\b|monthly|annually|enterprise|starter|pro\s+plan)\b/i, agents: ["onboard", "follow-up", "lead-grab"] },
  { id: "demo-cta",     label: "demo / sales CTA",     evidence: /\b(book\s+(?:a\s+)?demo|request\s+(?:a\s+)?demo|talk\s+to\s+sales|schedule\s+(?:a\s+)?call|get\s+a\s+quote)\b/i, agents: ["lead-grab", "calendar", "follow-up"] },
  { id: "contact-form", label: "contact form",         evidence: /\b(contact\s+us|get\s+in\s+touch|send\s+us\s+a\s+message|reach\s+out)\b/i, agents: ["intake", "lead-grab"] },
  { id: "lead-form",    label: "lead-capture form",    evidence: /\b(get\s+started|sign\s+up|join\s+the\s+(?:waitlist|beta)|request\s+access|free\s+(?:trial|consultation))\b/i, agents: ["lead-grab", "onboard", "follow-up"] },
  { id: "newsletter",   label: "email newsletter",     evidence: /\b(newsletter|subscribe\s+to|join\s+our\s+(?:list|community)|sign\s+up\s+for\s+(?:our|the))\b/i, agents: ["onboard", "follow-up"] },
  { id: "blog",         label: "active blog",          evidence: /\b(blog|articles?|insights\b|latest\s+posts?|read\s+(?:more|the\s+blog))\b/i, agents: ["intern", "standup"] },
  { id: "reviews",      label: "reviews / testimonials", evidence: /\b(reviews?\b|testimonials?|trustpilot|yelp\b|google\s+reviews|customer\s+stories)\b/i, agents: ["review-rep"] },
  { id: "case-studies", label: "case studies",         evidence: /\b(case\s+studies?|our\s+work|portfolio|past\s+projects)\b/i, agents: ["lead-grab", "intern"] },
  { id: "services",     label: "services menu",        evidence: /\b(services\b|what\s+we\s+do|offerings?|capabilities|how\s+it\s+works)\b/i, agents: ["intake", "lead-grab"] },
  { id: "faq",          label: "FAQ",                  evidence: /\b(faq\b|frequently\s+asked|common\s+questions?)\b/i, agents: ["wa-helper"] },
  { id: "careers",      label: "careers / hiring",     evidence: /\b(careers?\b|we'?re\s+hiring|join\s+(?:our|the)\s+team|open\s+(?:roles|positions)|jobs?\b)\b/i, agents: ["onboard", "intake"] },
  { id: "events",       label: "events / classes",     evidence: /\b(events?\b|workshops?|classes?\b|sessions?\b|webinars?)\b/i, agents: ["booker", "calendar", "follow-up"] },
  { id: "menu-food",    label: "food menu",            evidence: /\b(menu\b|our\s+dishes|cuisine|chef|tasting|prix\s+fixe|brunch|dinner\s+menu|cocktails)\b/i, agents: ["wa-helper", "booker"], industry: "Restaurant · catering" },
  { id: "delivery",     label: "delivery / shipping",  evidence: /\b(delivery|track\s+(?:my\s+)?order|shipping\s+(?:options|info))\b/i, agents: ["wa-helper", "refund"] },
  { id: "donate",       label: "donations",            evidence: /\b(donate\b|donations?\b|give\s+now|support\s+(?:us|our\s+work)|fundraising)\b/i, agents: ["intake", "follow-up"], industry: "Non-profit · charity" },
  { id: "patient",      label: "patient portal",       evidence: /\b(patient\s+portal|new\s+patient|insurance|copay|prescription)\b/i, agents: ["booker", "intake"], industry: "Healthcare · clinic" },
  { id: "legal-prac",   label: "legal practice areas", evidence: /\b(attorney|lawyer|practice\s+areas?|case\s+(?:review|evaluation)|legal\s+services?)\b/i, agents: ["contract", "intake"], industry: "Legal · law firm" },
  { id: "real-listings",label: "property listings",    evidence: /\b(listings?\b|properties\b|for\s+sale|for\s+rent|sq\.?\s*ft|sqft|bedroom)\b/i, agents: ["lead-grab", "calendar"], industry: "Real estate · property" },
  { id: "fitness-class",label: "fitness classes",      evidence: /\b(class\s+(?:schedule|times)|personal\s+train(?:er|ing)|workout|yoga|pilates|crossfit)\b/i, agents: ["booker", "follow-up"], industry: "Wellness · fitness" },
  { id: "edu-courses",  label: "courses",              evidence: /\b(courses?\b|enroll(?:ment)?|tuition|curriculum|students?\b|cohort)\b/i, agents: ["intake", "onboard"], industry: "Education · training" },
  // Software needs strong signals — generic "platform"/"api" hits too many B2B sites
  { id: "saas-product", label: "software product",     evidence: /\b(api\s+(?:docs|reference|key)|sdk\b|developer\s+(?:docs|portal)|github\.com|webhooks?\b|oauth\b|app\s+store)\b/i, agents: ["onboard", "intern"], industry: "Technology · software" },
  { id: "agency-work",  label: "agency engagements",   evidence: /\b(agency\b|brand\s+strategy|creative\s+direction|campaign\s+(?:work|management)|client\s+work)\b/i, agents: ["lead-grab", "intern", "standup"], industry: "Marketing · agency" },
  { id: "construction", label: "construction work",    evidence: /\b(construction|contractor|build(?:ing|s)?\b|renovation|remodel)\b/i, agents: ["intake", "calendar", "invoice-c"], industry: "Construction · trades" },
  { id: "ship-freight", label: "shipping / freight",   evidence: /\b(freight|logistics|shipping\s+(?:rates|quotes)|carriers?|tracking)\b/i, agents: ["wa-helper", "intake"], industry: "Logistics · transportation" },
  { id: "insurance",    label: "insurance products",   evidence: /\b(coverage|deductible|premium|claims?\b|policies\b|insure)\b/i, agents: ["intake", "doc-up", "follow-up"], industry: "Insurance" },

  // Industrial / engineering / B2B supply (catches sites like equipment vendors, distributors)
  { id: "engineering",  label: "engineering services", evidence: /\b(engineering\b|engineers?\s+(?:design|provide|deliver)|mechanical\b|electrical\s+engineering|epc\b|civil\s+engineering)\b/i, agents: ["intake", "calendar", "follow-up"], industry: "Manufacturing" },
  { id: "supply",       label: "supply / distribution", evidence: /\b(supply|supplier|distributor|wholesale|catalog(?:ue)?\b|datasheets?|spec\s+sheets?|product\s+catalog)\b/i, agents: ["lead-grab", "intake", "invoice-c"], industry: "Wholesale · distribution" },
  { id: "industrial",   label: "industrial equipment", evidence: /\b(industrial|equipment\b|machinery|valves?\b|pumps?\b|bearings?\b|fasteners?\b|generator|hvac|electrical\s+supply)\b/i, agents: ["lead-grab", "intake", "follow-up"], industry: "Manufacturing" },
  { id: "rfq",          label: "request-for-quote",     evidence: /\b(request\s+(?:a\s+)?quote|rfq\b|get\s+a\s+quote|request\s+pricing|bulk\s+pricing)\b/i, agents: ["lead-grab", "follow-up", "calendar"] },

  // ---- Weak fallbacks (only fire if nothing strong matched) ----
  { id: "tld-org",  label: "non-profit (.org)",        evidence: /\.org(?:\/|$|[^a-z])/i, agents: ["intake", "follow-up"], industry: "Non-profit · charity", weak: true },
  { id: "tld-edu",  label: "education (.edu)",         evidence: /\.edu(?:\/|$|[^a-z])/i, agents: ["intake", "onboard"], industry: "Education · training", weak: true },
  { id: "tld-gov",  label: "government (.gov)",        evidence: /\.gov(?:\/|$|[^a-z])/i, agents: ["intake", "doc-up"], industry: "Government · public sector", weak: true },
  { id: "tld-tech", label: "tech (.ai/.io/.dev)",      evidence: /\.(ai|io|app|dev)(?:\/|$|[^a-z])/i, agents: ["onboard", "intern"], industry: "Technology · software", weak: true },
];

function detectSignals(haystack) {
  const hits = BUSINESS_SIGNALS
    .map((sig) => {
      const m = haystack.match(sig.evidence);
      if (!m) return null;
      return { id: sig.id, label: sig.label, snippet: m[0].toLowerCase(), agents: sig.agents, industry: sig.industry, weak: !!sig.weak };
    })
    .filter(Boolean);
  // Dedupe by id and prefer non-weak.
  const seen = new Set();
  return hits.filter((s) => {
    if (seen.has(s.id)) return false;
    seen.add(s.id);
    return true;
  });
}

function normalizeUrl(raw) {
  if (!raw) return "";
  let u = raw.trim();
  if (!/^https?:\/\//i.test(u)) u = "https://" + u;
  try { return new URL(u).href; } catch { return ""; }
}

// Browser-side static pages can't fetch arbitrary URLs (CORS), so we go
// through public CORS-relay services. Try a couple in sequence — if all
// fail, the caller falls back to URL-only heuristics.
async function fetchSiteHTML(url) {
  const proxies = [
    (u) => `https://corsproxy.io/?${encodeURIComponent(u)}`,
    (u) => `https://api.allorigins.win/raw?url=${encodeURIComponent(u)}`,
    (u) => `https://api.codetabs.com/v1/proxy?quest=${encodeURIComponent(u)}`,
  ];
  for (const make of proxies) {
    try {
      const ctrl = new AbortController();
      const timer = setTimeout(() => ctrl.abort(), 7000);
      const res = await fetch(make(url), { signal: ctrl.signal });
      clearTimeout(timer);
      if (!res.ok) continue;
      const text = await res.text();
      if (text && text.length > 200) return text;
    } catch { /* try next */ }
  }
  return null;
}

// Pull a lot of meaningful content out of the HTML. The richer the
// haystack, the more accurate the signal detection. We grab title, meta
// descriptions, h1/h2/h3, hero paragraphs, body text, nav links, button
// text, list items, and ALL anchor href paths (e.g. /pricing, /book).
function extractFromHTML(html, url) {
  const doc = new DOMParser().parseFromString(html, "text/html");
  const sel = (s) => doc.querySelector(s);
  const all = (s) => Array.from(doc.querySelectorAll(s));

  // Drop noisy elements that pollute the haystack
  all("script, style, noscript").forEach((el) => el.remove());

  const cleanText = (el) => (el?.textContent || "").replace(/\s+/g, " ").trim();
  const title = cleanText(sel("title"));
  const ogTitle = (sel("meta[property='og:title']")?.getAttribute("content") || "").trim();
  const siteName = (sel("meta[property='og:site_name']")?.getAttribute("content") || sel("meta[name='application-name']")?.getAttribute("content") || "").trim();
  const lang = doc.documentElement.getAttribute("lang") || "";
  const geoCountry = (sel("meta[name='geo.country']")?.getAttribute("content") || sel("meta[name='geo.region']")?.getAttribute("content") || "").trim();
  const metaDesc = (
    sel("meta[name='description']")?.getAttribute("content")
    || sel("meta[property='og:description']")?.getAttribute("content")
    || sel("meta[name='twitter:description']")?.getAttribute("content")
    || ""
  ).trim();
  const h1 = all("h1").slice(0, 5).map(cleanText).filter(Boolean).join(" · ");
  const h2 = all("h2").slice(0, 8).map(cleanText).filter(Boolean).join(" · ");
  const h3 = all("h3").slice(0, 12).map(cleanText).filter(Boolean).join(" · ");
  const heroPara = all("h1 + p, h2 + p, .hero p, [class*=hero] p, header p, main p")
    .slice(0, 6)
    .map(cleanText)
    .filter((t) => t.length > 20 && t.length < 320)
    .join(" ");
  const navText = all("nav a, header a, [role='navigation'] a")
    .map(cleanText).filter((t) => t.length > 1 && t.length < 50).join(" ");
  const buttonText = all("button, a.btn, [role='button'], a.button, [class*='cta'] a, [class*='Cta'] a")
    .map(cleanText).filter((t) => t.length > 2 && t.length < 50).join(" ");
  const listItems = all("li").slice(0, 60).map(cleanText).filter((t) => t.length > 4 && t.length < 120).join(" · ");
  // Anchor hrefs are super useful — paths like /pricing, /book, /careers
  const hrefs = all("a[href]").map((a) => a.getAttribute("href") || "").filter((h) => h && !h.startsWith("javascript:")).join(" ");

  const haystack = [
    url, title, ogTitle, metaDesc,
    h1, h2, h3, heroPara,
    navText, buttonText, listItems, hrefs,
  ].join(" ").toLowerCase();

  return {
    title: title || ogTitle,
    siteName,
    lang: (lang || "").toLowerCase(),
    geoCountry,
    description: metaDesc,
    h1,
    heroPara,
    navTokens: navText,
    haystack,
    bytes: html.length,
  };
}

// ============ LOCATION → TIMEZONE DETECTION ============
// Look at the haystack + hostname + html lang for hints about where the
// business operates, then map to one of the time zones in the dropdown.
const LOCATION_HINTS = [
  // Major US cities (most specific first)
  { match: /\b(new\s*york|nyc\b|manhattan|brooklyn|queens|miami|boston|atlanta|washington\s+dc|philadelphia)\b/i, tz: "Eastern (New York · UTC−5)" },
  { match: /\b(los\s+angeles|san\s+francisco|silicon\s+valley|seattle|portland|san\s+diego|sf\b)\b/i, tz: "Pacific (Los Angeles · UTC−8)" },
  { match: /\b(chicago|dallas|houston|austin|minneapolis|st\.?\s+louis|nashville)\b/i, tz: "Central (Chicago · UTC−6)" },
  { match: /\b(denver|salt\s+lake|phoenix)\b/i, tz: "Mountain (Denver · UTC−7)" },
  // Canada / South America
  { match: /\bhalifax\b/i, tz: "Atlantic (Halifax · UTC−4)" },
  { match: /\b(mexico\s+city|cdmx)\b/i, tz: "Mexico City · UTC−6" },
  { match: /\b(bogot[áa])\b/i, tz: "Bogotá · UTC−5" },
  { match: /\blima\b/i, tz: "Lima · UTC−5" },
  { match: /\bsantiago\b/i, tz: "Santiago · UTC−4" },
  { match: /\b(s[ãa]o\s+paulo|rio\s+de\s+janeiro)\b/i, tz: "São Paulo · UTC−3" },
  { match: /\b(buenos\s+aires|argentina)\b/i, tz: "Buenos Aires · UTC−3" },
  // Europe
  { match: /\b(reykjav[íi]k|iceland)\b/i, tz: "Reykjavík · UTC+0" },
  { match: /\b(london|england|britain|uk\b|united\s+kingdom)\b/i, tz: "London · UTC+0/+1" },
  { match: /\b(lisbon|portugal)\b/i, tz: "Lisbon · UTC+0/+1" },
  { match: /\b(paris|france|french)\b/i, tz: "Paris · UTC+1/+2" },
  { match: /\b(berlin|munich|frankfurt|hamburg|germany|deutschland)\b/i, tz: "Berlin · UTC+1/+2" },
  { match: /\b(madrid|barcelona|spain)\b/i, tz: "Madrid · UTC+1/+2" },
  { match: /\b(rome|milan|italy)\b/i, tz: "Rome · UTC+1/+2" },
  { match: /\b(amsterdam|netherlands|holland)\b/i, tz: "Amsterdam · UTC+1/+2" },
  { match: /\b(copenhagen|denmark)\b/i, tz: "Copenhagen · UTC+1/+2" },
  { match: /\b(athens|greece)\b/i, tz: "Athens · UTC+2/+3" },
  { match: /\b(helsinki|finland)\b/i, tz: "Helsinki · UTC+2/+3" },
  { match: /\b(istanbul|turkey)\b/i, tz: "Istanbul · UTC+3" },
  { match: /\b(moscow|russia)\b/i, tz: "Moscow · UTC+3" },
  // Africa
  { match: /\b(casablanca|morocco)\b/i, tz: "Casablanca · UTC+1" },
  { match: /\b(lagos|nigeria)\b/i, tz: "Lagos · UTC+1" },
  { match: /\b(cairo|egypt|alexandria)\b/i, tz: "Cairo · UTC+2" },
  { match: /\b(johannesburg|south\s+africa|cape\s+town)\b/i, tz: "Johannesburg · UTC+2" },
  { match: /\b(nairobi|kenya)\b/i, tz: "Nairobi · UTC+3" },
  // Middle East
  { match: /\b(tel\s+aviv|jerusalem|israel)\b/i, tz: "Tel Aviv · UTC+2/+3" },
  { match: /\b(riyadh|jeddah|saudi\s+arabia|ksa\b)\b/i, tz: "Riyadh · UTC+3" },
  { match: /\b(tehran|iran)\b/i, tz: "Tehran · UTC+3:30" },
  { match: /\b(dubai|abu\s+dhabi|uae|emirates)\b/i, tz: "Dubai · UTC+4" },
  // Asia
  { match: /\b(karachi|lahore|pakistan)\b/i, tz: "Karachi · UTC+5" },
  { match: /\b(mumbai|delhi|bangalore|bengaluru|hyderabad|india|chennai)\b/i, tz: "Mumbai · UTC+5:30" },
  { match: /\b(dhaka|bangladesh)\b/i, tz: "Dhaka · UTC+6" },
  { match: /\b(bangkok|thailand)\b/i, tz: "Bangkok · UTC+7" },
  { match: /\b(jakarta|indonesia)\b/i, tz: "Jakarta · UTC+7" },
  { match: /\bsingapore\b/i, tz: "Singapore · UTC+8" },
  { match: /\b(hong\s+kong|hk\b)\b/i, tz: "Hong Kong · UTC+8" },
  { match: /\b(shanghai|beijing|china|guangzhou|shenzhen)\b/i, tz: "Shanghai · UTC+8" },
  { match: /\b(seoul|korea)\b/i, tz: "Seoul · UTC+9" },
  { match: /\b(tokyo|osaka|japan)\b/i, tz: "Tokyo · UTC+9" },
  // Oceania
  { match: /\b(perth|western\s+australia)\b/i, tz: "Perth · UTC+8" },
  { match: /\b(sydney|melbourne|brisbane|australia)\b/i, tz: "Sydney · UTC+10/+11" },
  { match: /\b(auckland|wellington|new\s+zealand|nz\b)\b/i, tz: "Auckland · UTC+12/+13" },
  // Phone country codes (last-resort, less reliable)
  { match: /\+44[\s\-]/, tz: "London · UTC+0/+1" },
  { match: /\+33[\s\-]/, tz: "Paris · UTC+1/+2" },
  { match: /\+49[\s\-]/, tz: "Berlin · UTC+1/+2" },
  { match: /\+39[\s\-]/, tz: "Rome · UTC+1/+2" },
  { match: /\+34[\s\-]/, tz: "Madrid · UTC+1/+2" },
  { match: /\+20[\s\-]/, tz: "Cairo · UTC+2" },
  { match: /\+971[\s\-]/, tz: "Dubai · UTC+4" },
  { match: /\+966[\s\-]/, tz: "Riyadh · UTC+3" },
  { match: /\+91[\s\-]/, tz: "Mumbai · UTC+5:30" },
  { match: /\+81[\s\-]/, tz: "Tokyo · UTC+9" },
  { match: /\+86[\s\-]/, tz: "Shanghai · UTC+8" },
  { match: /\+65[\s\-]/, tz: "Singapore · UTC+8" },
  // ccTLDs
  { tld: /\.(uk|co\.uk)$/i, tz: "London · UTC+0/+1" },
  { tld: /\.(de)$/i, tz: "Berlin · UTC+1/+2" },
  { tld: /\.(fr)$/i, tz: "Paris · UTC+1/+2" },
  { tld: /\.(es)$/i, tz: "Madrid · UTC+1/+2" },
  { tld: /\.(it)$/i, tz: "Rome · UTC+1/+2" },
  { tld: /\.(nl)$/i, tz: "Amsterdam · UTC+1/+2" },
  { tld: /\.(eg)$/i, tz: "Cairo · UTC+2" },
  { tld: /\.(ae)$/i, tz: "Dubai · UTC+4" },
  { tld: /\.(sa)$/i, tz: "Riyadh · UTC+3" },
  { tld: /\.(in)$/i, tz: "Mumbai · UTC+5:30" },
  { tld: /\.(jp)$/i, tz: "Tokyo · UTC+9" },
  { tld: /\.(cn)$/i, tz: "Shanghai · UTC+8" },
  { tld: /\.(au)$/i, tz: "Sydney · UTC+10/+11" },
  { tld: /\.(nz)$/i, tz: "Auckland · UTC+12/+13" },
  { tld: /\.(za)$/i, tz: "Johannesburg · UTC+2" },
];

function detectLocation(haystack, host, lang, geoCountry) {
  const stack = haystack + " " + host.toLowerCase() + " " + (lang || "") + " " + (geoCountry || "").toLowerCase();
  // Try city/country first, then phone code, then TLD as the weakest cue
  for (const h of LOCATION_HINTS) {
    if (h.match && h.match.test(stack)) return h.tz;
  }
  for (const h of LOCATION_HINTS) {
    if (h.tld && h.tld.test(host)) return h.tz;
  }
  return null;
}

// Pull a clean company name from the page metadata. Prefer og:site_name
// (most explicit), then strip common cruft from <title> tags like
// " — Site Pro" / " | Home" / " - Welcome".
function detectCompanyName(extracted, host) {
  if (!extracted) return null;
  if (extracted.siteName && extracted.siteName.length > 1 && extracted.siteName.length < 60) return extracted.siteName.trim();
  const title = (extracted.title || "").trim();
  if (!title) return null;
  // Split on common separators
  const parts = title.split(/\s+[\-–|·•]\s+/);
  const longest = parts.sort((a, b) => b.length - a.length)[0]; // pick the most descriptive segment
  const first = parts[0];
  // The earlier segment is usually the company name; if it's super short
  // or generic, fall back to the longest meaningful segment.
  let candidate = first;
  if (first.length < 3 || /^(home|welcome|index)$/i.test(first)) candidate = longest;
  candidate = candidate.replace(/^\s*(welcome\s+to\s+)/i, "").trim();
  if (candidate.length < 2 || candidate.length > 60) return null;
  return candidate;
}

// ============ CLAUDE-POWERED ANALYSIS (when API key is set) ============
// When the user has pasted an Anthropic API key in Settings, use Claude to
// extract a real, accurate analysis from the page content. This is what
// gives output like "Gabtic, electro-mechanical engineering for 40 years
// in Giza, Egypt" instead of "gabtic is a small business".
async function analyzeWithClaude(extracted, url, apiKey, industries, timezones) {
  // Build a compact "page brief" from the extracted bits — enough signal
  // for Claude to reason over, while staying inside reasonable input sizes.
  const pageBrief = [
    `URL: ${url}`,
    extracted.title    && `Title: ${extracted.title}`,
    extracted.siteName && `Site name (og): ${extracted.siteName}`,
    extracted.description && `Meta description: ${extracted.description}`,
    extracted.h1       && `H1: ${extracted.h1}`,
    extracted.heroPara && `Hero text: ${extracted.heroPara}`,
    extracted.lang     && `HTML lang: ${extracted.lang}`,
    extracted.geoCountry && `Geo meta: ${extracted.geoCountry}`,
    extracted.navTokens && `Navigation: ${extracted.navTokens.slice(0, 600)}`,
    extracted.haystack && `Body text (excerpt): ${extracted.haystack.slice(0, 4000)}`,
  ].filter(Boolean).join("\n\n");

  const system = `You analyze business websites for a CRM that recommends AI agents.

Return ONLY a JSON object — no prose, no markdown fence. The JSON must match this schema:
{
  "companyName": string,            // Real, clean company name. No taglines, no "Welcome to".
  "summary": string,                // 2-3 sentences. Be specific: name actual products, services, locations, years in business if mentioned. NEVER write "looks like a small business".
  "industry": string|null,          // EXACT match from this list, or null if unsure: ${industries.join(" | ")}
  "tz": string|null,                // EXACT match from this list, or null: ${timezones.join(" | ")}
  "location": string|null,          // City, Country (e.g. "Giza, Egypt") if anything in the page suggests it.
  "products": string[],             // Specific products / services they sell. Empty array if not stated.
  "notableProjects": string[],      // Specific named projects, clients, or works. Empty if none mentioned.
  "leadership": string[],           // Names + titles if any are on the page (e.g. "Hussein El Alfy — CEO"). Empty if none.
  "signals": [                      // Capabilities visible on the site that suggest the right agents to hire.
    { "label": string, "evidence": string }    // label = short capability name; evidence = exact phrase from the page.
  ]
}

Use ONLY facts that appear in the page content provided. Don't invent. If you can't tell, use null or [].`;

  const SERVER_URL = (window.CITRUS_CONFIG && window.CITRUS_CONFIG.SERVER_URL) || "http://localhost:3001";
  const res = await fetch(`${SERVER_URL}/api/chat`, {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify({
      model: "claude-haiku-4-5-20251001",
      max_tokens: 1500,
      system,
      messages: [{ role: "user", content: `Page brief for ${url}:\n\n${pageBrief}` }],
    }),
  });

  if (!res.ok) {
    const errText = await res.text().catch(() => "");
    throw new Error(`Server /api/chat ${res.status}: ${errText.slice(0, 200)}`);
  }
  const data = await res.json();
  const text = (data.content && data.content[0] && data.content[0].text) || "";
  // Strip optional ```json fences
  const stripped = text.trim().replace(/^```(?:json)?\s*/i, "").replace(/```\s*$/i, "").trim();
  // Find the first {...} block in case the model wrapped it
  const jsonStart = stripped.indexOf("{");
  const jsonEnd   = stripped.lastIndexOf("}");
  const jsonStr = jsonStart >= 0 && jsonEnd >= 0 ? stripped.slice(jsonStart, jsonEnd + 1) : stripped;
  return JSON.parse(jsonStr);
}

async function analyzeWebsite(rawUrl) {
  const url = normalizeUrl(rawUrl);
  if (!url) return null;
  let host = "";
  try { host = new URL(url).hostname; } catch {}
  const niceHost = host.replace(/^www\./, "");

  // 1) Read the site through a CORS proxy
  const html = await fetchSiteHTML(url);
  let extracted = null;
  let haystack;
  if (html) {
    extracted = extractFromHTML(html, url);
    haystack = extracted.haystack;
  } else {
    haystack = (host + " " + url).toLowerCase();
  }

  // 2) If Claude is configured, ask Claude — much better quality than heuristics
  // Prefer .env-injected key (window.CITRUS_CONFIG) so a server-side .env can
  // power analysis without the user touching Settings.
  const apiKey = (() => {
    const fromEnv = (typeof window !== "undefined" && window.CITRUS_CONFIG && window.CITRUS_CONFIG.CLAUDE_API_KEY) || "";
    if (fromEnv) return fromEnv;
    try { return localStorage.getItem("citrus_anthropic_key") || ""; } catch { return ""; }
  })();

  if (apiKey && extracted) {
    try {
      const ai = await analyzeWithClaude(extracted, url, apiKey, INDUSTRIES, ALL_TIMEZONES);
      // Map Claude's signals (label + evidence) to our agent archetypes by
      // matching the label/evidence text against BUSINESS_SIGNALS regexes.
      const aiSignals = (ai.signals || []).map((s, i) => {
        const text = ((s.label || "") + " " + (s.evidence || "")).toLowerCase();
        const matched = BUSINESS_SIGNALS.find((d) => d.evidence.test(text));
        return {
          id: matched ? matched.id : "ai-sig-" + i,
          label: s.label || (matched && matched.label) || "capability",
          snippet: (s.evidence || "").slice(0, 80),
          agents: matched ? matched.agents : [],
          industry: matched && matched.industry,
          weak: false,
        };
      }).filter((s) => s.label);
      return {
        url,
        host: niceHost,
        companyName: ai.companyName || detectCompanyName(extracted, niceHost) || niceHost,
        industry: (ai.industry && INDUSTRIES.includes(ai.industry)) ? ai.industry : null,
        tz: (ai.tz && ALL_TIMEZONES.includes(ai.tz)) ? ai.tz : null,
        summary: ai.summary || extracted.description || extracted.title,
        signals: aiSignals.slice(0, 8),
        title: extracted.title || "",
        description: extracted.description || "",
        location: ai.location || null,
        products: ai.products || [],
        notableProjects: ai.notableProjects || [],
        leadership: ai.leadership || [],
        sourcedFromSite: true,
        poweredBy: "claude",
      };
    } catch (err) {
      // Surface Claude failure but fall back to heuristics so the user still gets something
      window.toast && window.toast(`Claude call failed — using heuristics. ${String(err.message || err).slice(0, 120)}`, "warn");
    }
  }

  // 3) HEURISTIC PATH (no API key, or Claude failed)
  let detectedSignals = detectSignals(haystack);
  const strong = detectedSignals.filter((s) => !s.weak);
  const used = strong.length > 0 ? strong : detectedSignals;
  detectedSignals = used;

  const industryVotes = {};
  detectedSignals.forEach((s) => {
    if (s.industry) industryVotes[s.industry] = (industryVotes[s.industry] || 0) + 1;
  });
  const industry = Object.keys(industryVotes).sort((a, b) => industryVotes[b] - industryVotes[a])[0] || null;

  let summary;
  if (extracted && (extracted.description || extracted.heroPara || extracted.title)) {
    summary = extracted.description || extracted.heroPara || extracted.title;
    if (summary.length > 240) summary = summary.slice(0, 237).trim() + "…";
  } else if (industry || detectedSignals.length) {
    const sigLabels = detectedSignals.slice(0, 2).map((s) => s.label);
    summary = `${niceHost} looks like `;
    summary += industry ? `a ${industry.toLowerCase().split(" · ")[0]} business` : "a small business";
    if (sigLabels.length) summary += ` with ${sigLabels.join(" and ")}`;
    summary += ".";
  } else {
    summary = `We reached ${niceHost} but couldn't pick out enough signal. Add a Claude API key below for full analysis, or describe the business in the field below.`;
  }

  const companyName = detectCompanyName(extracted, niceHost);
  const tz = detectLocation(haystack, host, extracted?.lang, extracted?.geoCountry);

  return {
    url,
    host: niceHost,
    industry,
    signals: detectedSignals.slice(0, 8),
    summary,
    title: extracted?.title || "",
    description: extracted?.description || "",
    companyName,
    tz,
    location: null,
    products: [],
    notableProjects: [],
    leadership: [],
    sourcedFromSite: !!html,
    poweredBy: "heuristics",
  };
}

// Build a tailored "why" using the actual signals that drove this rec.
// e.g. "Because you have a pricing page and a demo CTA, this agent will
// follow up with every demo request three polite touches."
function evidenceWhy(id, drivers, industry) {
  if (drivers.length === 0) return whyFor(id, industry);
  const labels = drivers.map((d) => d.label);
  const list = labels.length === 1 ? labels[0]
              : labels.length === 2 ? labels.join(" and ")
              : labels.slice(0, 2).join(", ") + " (and more)";
  const tplByAgent = {
    "wa-helper":  `You have ${list} — this agent answers customers on WhatsApp 24/7 and routes the leads to your phone.`,
    "booker":     `Because your site has ${list}, this agent confirms bookings without back-and-forth and never overbooks.`,
    "lead-grab":  `You have ${list} — this agent captures every fill, replies in seconds, and logs the lead for sales.`,
    "follow-up":  `With ${list} on your site, this agent runs three polite touches on every untouched lead and stops the second they reply.`,
    "monday-an":  `Given your ${list}, this agent writes a plain-English Monday recap so you don't drown in dashboards.`,
    "invoice-c":  `You have ${list} — this agent chases overdue invoices on a polite cadence and pauses when a conversation is open.`,
    "refund":     `Because of your ${list}, this agent drafts refund replies and only loops you in for the big ones.`,
    "review-rep": `You have ${list} — this agent replies to every review in your voice and flags the bad ones.`,
    "contract":   `Because you handle ${list}, this agent reads every contract before you sign and tells you in plain English where the weird stuff is.`,
    "calendar":   `With ${list} on your site, this agent books meetings around your buffers and protected time.`,
    "intern":     `Given your ${list}, this agent researches anything you forward and sends a one-pager.`,
    "standup":    `You have ${list} — this agent summarizes team activity into one short Monday roll-up.`,
    "onboard":    `Because your site has ${list}, this agent welcomes new sign-ups, gets them to first value, and checks in at day 7.`,
    "doc-up":     `You handle ${list} — this agent triages every document the moment it arrives and tells you what's needed.`,
    "intake":     `With ${list} bringing in inbound queries, this agent qualifies, captures, and routes them to the right place.`,
  };
  return tplByAgent[id] || whyFor(id, industry);
}

function recommendAgents(industry, description, signals) {
  const baseIds = INDUSTRY_RECS[industry] || INDUSTRY_RECS["Other"];
  const scores = {};
  // Industry priors (low weight)
  baseIds.forEach((id, i) => { scores[id] = (baseIds.length - i) * 2; });
  // Signals from the website (high weight — these are real evidence)
  const driversByAgent = {};
  (signals || []).forEach((sig) => {
    (sig.agents || []).forEach((id) => {
      scores[id] = (scores[id] || 0) + 5;
      if (!driversByAgent[id]) driversByAgent[id] = [];
      driversByAgent[id].push(sig);
    });
  });
  // Description keywords (medium weight)
  const desc = (description || "").toLowerCase();
  KEYWORD_BUMPS.forEach(([re, ids]) => {
    if (re.test(desc)) ids.forEach((id) => { scores[id] = (scores[id] || 0) + 3; });
  });
  return Object.keys(scores)
    .filter((id) => AGENT_ARCHETYPES[id])
    .sort((a, b) => scores[b] - scores[a])
    .slice(0, 4)
    .map((id) => {
      const drivers = (driversByAgent[id] || []).slice(0, 3);
      return { id, ...AGENT_ARCHETYPES[id], why: evidenceWhy(id, drivers, industry), drivers };
    });
}

const PROVIDERS = [
  {
    id: "claude",
    name: "Claude",
    tag: "Anthropic",
    desc: "Reasoning-heavy default. Best for careful judgment, long context, and polite back-and-forth with customers.",
    envKey: "CLAUDE_API_KEY",
    envModel: "CLAUDE_MODEL",
    models: ["claude-opus-4-7", "claude-sonnet-4-6", "claude-haiku-4-5"],
    docsLabel: "console.anthropic.com",
    docsUrl: "https://console.anthropic.com/",
    loaded: true, // mocked: pretend .env has this one filled in
  },
  {
    id: "chatgpt",
    name: "ChatGPT",
    tag: "OpenAI · Codex",
    desc: "Strong on code-heavy tasks (Codex) and high-throughput drafting. Use GPT-5 for general reasoning, Codex for anything that has to write or edit code.",
    envKey: "OPENAI_API_KEY",
    envModel: "OPENAI_MODEL",
    models: ["gpt-5-codex", "gpt-5", "gpt-4o"],
    docsLabel: "platform.openai.com",
    docsUrl: "https://platform.openai.com/",
    loaded: false,
  },
  {
    id: "custom",
    name: "Bring your own",
    tag: "Self-hosted",
    desc: "Any OpenAI-compatible endpoint — vLLM, Ollama, Azure OpenAI, your own fine-tune. Useful for air-gapped setups.",
    envKey: "CUSTOM_API_KEY",
    envModel: "CUSTOM_MODEL",
    envEndpoint: "CUSTOM_API_BASE",
    models: ["(detected from /v1/models)"],
    docsLabel: null,
    loaded: false,
  },
];

function ModelProviderSection() {
  // Mocked here; production reads CITRUS_PROVIDER from .env at request time.
  const [active, setActive] = useState("claude");

  return (
    <>
      <h3 className="st-pane-h">Model provider</h3>
      <p className="st-pane-s">
        Pick which model powers your agents. Citrus reads credentials from
        a <code className="st-model-code">.env</code> file at the project
        root — copy <code className="st-model-code">.env.example</code> to{" "}
        <code className="st-model-code">.env</code> and fill in the keys for
        whichever providers you want to use.
      </p>

      <div className="st-model-list">
        {PROVIDERS.map(p => {
          const isActive = active === p.id;
          return (
            <div key={p.id} className={`st-model-card ${isActive ? "is-active" : ""}`}>
              <label className="st-model-head">
                <input
                  type="radio"
                  name="provider"
                  className="st-model-radio"
                  checked={isActive}
                  onChange={() => setActive(p.id)}
                />
                <div className="st-model-id">
                  <div className="st-model-name">
                    {p.name}
                    <span className="st-model-tag">{p.tag}</span>
                  </div>
                  <div className="st-model-desc">{p.desc}</div>
                </div>
                <div className="st-model-status">
                  {p.loaded
                    ? <span className="st-model-status-pip is-ok">Loaded from .env</span>
                    : <span className="st-model-status-pip is-off">Missing in .env</span>}
                </div>
              </label>

              <div className="st-model-fields">
                <div className="st-model-env">
                  {p.envEndpoint && (
                    <div className="st-model-env-row">
                      <span className="st-model-env-k">{p.envEndpoint}</span>
                      <span className="st-model-env-v">{p.loaded ? "https://your-host/v1" : "—"}</span>
                    </div>
                  )}
                  <div className="st-model-env-row">
                    <span className="st-model-env-k">{p.envKey}</span>
                    <span className="st-model-env-v">{p.loaded ? "•••••••••••••••3a9f" : "—"}</span>
                  </div>
                  <div className="st-model-env-row">
                    <span className="st-model-env-k">{p.envModel}</span>
                    <span className="st-model-env-v">{p.loaded ? p.models[0] : "—"}</span>
                  </div>
                </div>

                <div className="st-model-foot">
                  <div className="st-model-foot-l">
                    Available models:{" "}
                    {p.models.map((m, i) => (
                      <code key={m} className="st-model-code">{m}{i < p.models.length - 1 ? ", " : ""}</code>
                    ))}
                    {p.docsUrl && <> · <a href={p.docsUrl} target="_blank" rel="noopener" className="st-model-link">Get a key from {p.docsLabel} ↗</a></>}
                  </div>
                  <div className="st-model-foot-r">
                    {!isActive && p.loaded && (
                      <button type="button" className="btn btn-ghost btn-sm" onClick={() => setActive(p.id)}>Set as default</button>
                    )}
                    {!p.loaded && (
                      <span className="st-model-hint">Add <code className="st-model-code">{p.envKey}</code> to <code className="st-model-code">.env</code></span>
                    )}
                  </div>
                </div>
              </div>
            </div>
          );
        })}
      </div>

      <div className="st-model-tip">
        <div className="st-model-tip-mark">i</div>
        <div>
          <div className="st-model-tip-t">Where does <code className="st-model-code">.env</code> live?</div>
          <div className="st-model-tip-s">
            Project root, next to <code className="st-model-code">README.md</code>.
            See <code className="st-model-code">.env.example</code> for the full list of keys (CLAUDE_API_KEY, OPENAI_API_KEY, CUSTOM_API_BASE, CITRUS_PROVIDER).
            Never commit your real <code className="st-model-code">.env</code> — add it to <code className="st-model-code">.gitignore</code>.
          </div>
        </div>
      </div>
    </>
  );
}

function SettingsTab({ onNew, onEnvsChange }) {
  const [section, setSection] = useState("profile");
  const sections = [
    ["profile",       "Company"],
    ["voice",         "Voice & rules"],
    ["learning",      "Learning"],
    ["notifications", "Lead notifications"],
    ["model",         "Model"],
    ["api",           "API keys"],
    ["environments",  "Environments"],
    ["invoices",      "Invoices"],
    ["danger",        "Danger zone"],
  ];

  // Lead-manager notification settings live on the runtime server, not in
  // localStorage — every conversation wrap-up needs them, and we want them
  // to persist if you're running on a real machine. Hydrate on mount.
  const SERVER_URL_NOTIF = (typeof window !== "undefined" && window.CITRUS_CONFIG && window.CITRUS_CONFIG.SERVER_URL) || "";
  const [leadEmail, setLeadEmail]         = useState("");
  const [leadVerifiedAt, setLeadVerifiedAt] = useState(null);
  const [leadDraft, setLeadDraft]         = useState("");
  const [leadCode, setLeadCode]           = useState("");
  const [leadPhase, setLeadPhase]         = useState("idle"); // idle | sending | code-sent | verifying | verified
  const [leadErr, setLeadErr]             = useState("");

  // ---- LINKED GMAIL ----
  // Per-tenant OAuth: connect a Gmail so wrap-up recaps, team invites,
  // and verification codes are sent from the tenant's own address. Falls
  // back to the global SHEETS_WEBHOOK_URL when nothing's linked.
  const [gmailLinked, setGmailLinked]   = useState(null);  // null = loading, false = not linked, true = linked
  const [gmailEmail, setGmailEmail]     = useState("");
  const [gmailConnectedAt, setGmailConnectedAt] = useState(null);
  const [gmailBusy, setGmailBusy]       = useState(false);
  const [gmailErr, setGmailErr]         = useState("");
  // Refresh status on mount + when the OAuth callback bounces back here.
  const refreshGmail = () => {
    fetch(`${SERVER_URL_NOTIF}/config/gmail/status`)
      .then((r) => r.ok ? r.json() : null)
      .then((d) => {
        if (!d) return;
        setGmailLinked(!!d.connected);
        setGmailEmail(d.email || "");
        setGmailConnectedAt(d.connectedAt || null);
      })
      .catch(() => setGmailLinked(false));
  };
  useEffect(() => {
    refreshGmail();
    // OAuth callback bounces back to /dashboard/?gmail_link=ok&email=...
    // Show the user a toast and clear the query param so a reload doesn't
    // re-trigger it.
    try {
      const u = new URL(window.location.href);
      const flag = u.searchParams.get("gmail_link");
      if (flag === "ok") {
        const email = u.searchParams.get("email") || "your Gmail";
        if (window.toast) window.toast(`Linked ${email}`, "good");
      } else if (flag) {
        setGmailErr(flag === "no-refresh-token"
          ? "Google didn't return a refresh token. Disconnect this app from your Google account (myaccount.google.com → Security → Third-party apps), then try again."
          : `Couldn't link Gmail: ${flag}`);
      }
      if (flag) { u.searchParams.delete("gmail_link"); u.searchParams.delete("email"); window.history.replaceState(null, "", u.pathname + (u.search ? "?" + u.searchParams.toString() : "") + u.hash); }
    } catch { /* ignore */ }
  }, []);
  const connectGmail = () => {
    setGmailErr("");
    // Full-page navigation can't carry the Authorization or X-Tenant-Slug
    // headers, so we attach both as query params. requireAuth accepts
    // ?token= (same as SSE clients) and applyTenantSlugOverride accepts
    // ?_tenant= so the linked Gmail lands on the impersonated tenant
    // (otherwise a MasterAdmin viewing /t/nokia/ would link Gmail to
    // "self" instead of Nokia).
    const token = (typeof localStorage !== "undefined" && localStorage.getItem("citrus_auth_token")) || "";
    const slug  = (typeof window !== "undefined" && window.__citrusTenantSlug) || "";
    const returnTo = window.location.pathname + window.location.search + window.location.hash;
    const params = new URLSearchParams();
    if (token) params.set("token", token);
    if (slug)  params.set("_tenant", slug);
    params.set("returnTo", returnTo);
    window.location.href = `${SERVER_URL_NOTIF}/config/gmail/connect?${params.toString()}`;
  };
  const disconnectGmail = async () => {
    if (!window.confirm(`Unlink ${gmailEmail || "the connected Gmail"}? Future emails will fall back to the global webhook (or fail if none is configured).`)) return;
    setGmailBusy(true);
    try {
      await fetch(`${SERVER_URL_NOTIF}/config/gmail/disconnect`, { method: "POST" });
      setGmailLinked(false); setGmailEmail(""); setGmailConnectedAt(null);
    } catch (e) { setGmailErr(e.message); }
    finally { setGmailBusy(false); }
  };

  const [conflictMode, setConflictMode]   = useState("manual");
  const [conflictSaving, setConflictSaving] = useState(false);

  const [memoryMode, setMemoryMode]                   = useState("scheduled");
  const [memoryInterval, setMemoryInterval]           = useState(15);
  const [vipFactThreshold, setVipFactThreshold]       = useState(5);
  const [memoryDecayDays, setMemoryDecayDays]         = useState(90);
  const [memoryPrecedence, setMemoryPrecedence]       = useState(true);
  const [memoryConflictDet, setMemoryConflictDet]     = useState(true);
  const [memorySaving, setMemorySaving]               = useState(false);

  // ---- ENVIRONMENTS ----
  // The environment list is server-side (shared across all team members).
  // activeEnvId stays in localStorage — it's per-user (each person picks their own env).
  const [envs, setEnvs] = React.useState([]);
  const [envRequests, setEnvRequests] = React.useState([]);
  const [envsLoaded, setEnvsLoaded] = React.useState(false);
  const [activeEnvId, setActiveEnvIdState] = React.useState(() => localStorage.getItem("citrus_active_env") || "");
  const [envRequestDraft, setEnvRequestDraft] = React.useState({ name: "", type: "sandbox", reason: "" });
  const [envRefreshing, setEnvRefreshing] = React.useState(false);
  const [envRefreshingId, setEnvRefreshingId] = React.useState(null);
  const [envMsg, setEnvMsg] = React.useState("");

  const envPointsAtPrimary = (env) => {
    const base = String(env?.url || "").replace(/\/$/, "");
    const primary = String(window.__citrusPrimaryUrl || window.location.origin || "").replace(/\/$/, "");
    return !!base && !!primary && (base === primary || base.startsWith(primary + "/"));
  };

  // Load environments from server on mount; migrate any localStorage envs on first load.
  // Always use relative URLs — environments live on the primary server, not the active env's server.
  React.useEffect(() => {
    fetch("/admin/environments")
      .then((r) => r.json())
      .then((data) => {
        const serverEnvs = data.environments || [];
        // One-time migration: push any localStorage envs not already on the server.
        const local = (() => { try { return JSON.parse(localStorage.getItem("citrus_envs") || "[]"); } catch { return []; } })();
        const toMigrate = local.filter((l) => !serverEnvs.find((s) => s.id === l.id));
        if (toMigrate.length > 0) {
          fetch("/admin/environments", {
            method: "POST",
            headers: { "content-type": "application/json" },
            body: JSON.stringify({ environments: toMigrate }),
          })
            .then(async (r) => {
              const d = await r.json().catch(() => ({}));
              if (!r.ok) throw new Error(d.error || r.status);
              return d;
            })
            .then((d) => { setEnvs(d.environments || []); localStorage.removeItem("citrus_envs"); })
            .catch(() => setEnvs(serverEnvs));
        } else {
          setEnvs(serverEnvs);
          localStorage.removeItem("citrus_envs");
        }
      })
      .catch(() => {
        try { setEnvs(JSON.parse(localStorage.getItem("citrus_envs") || "[]")); } catch {}
      })
      .finally(() => setEnvsLoaded(true));
    fetch("/admin/environment-requests")
      .then((r) => r.ok ? r.json() : { requests: [] })
      .then((data) => setEnvRequests(data.requests || []))
      .catch(() => setEnvRequests([]));
  }, []);

  const setActiveEnvId = (id, envRecord) => {
    setActiveEnvIdState(id);
    if (id) {
      localStorage.setItem("citrus_active_env", id);
      // Store the full env record so applyActiveEnv can configure SERVER_URL on reload
      const record = envRecord || envs.find((e) => e.id === id);
      if (record) localStorage.setItem("citrus_active_env_data", JSON.stringify(record));
    } else {
      localStorage.removeItem("citrus_active_env");
      localStorage.removeItem("citrus_active_env_data");
    }
  };

  const requestEnvironment = async () => {
    if (!envRequestDraft.name.trim()) return;
    setEnvMsg("");
    try {
      const r = await fetch("/admin/environment-requests", {
        method: "POST",
        headers: { "content-type": "application/json" },
        body: JSON.stringify({
          name: envRequestDraft.name.trim(),
          type: envRequestDraft.type,
          reason: envRequestDraft.reason,
          sourceEnvironmentId: "production",
        }),
      });
      const j = await r.json();
      if (!r.ok) throw new Error(j.error || r.status);
      setEnvRequests((xs) => [j.request, ...xs]);
      setEnvRequestDraft({ name: "", type: "sandbox", reason: "" });
      setEnvMsg("Request sent. Citrus will provision it from master admin.");
    } catch (e) {
      setEnvMsg("Request failed: " + e.message);
    }
  };

  const switchEnv = (id) => {
    const record = envs.find((e) => e.id === id) || null;
    setActiveEnvId(id || "", record);
    window.location.reload();
  };

  const doRefresh = async (targetEnv) => {
    if (!targetEnv || targetEnv.type === "production") return;
    if (envPointsAtPrimary(targetEnv)) {
      setEnvMsg("Refresh failed: this environment points at Production. Ask Citrus to attach the real sandbox runtime first.");
      return;
    }
    if (!window.confirm(`Refresh ${targetEnv.name} from Production? This replaces agents, knowledge, learnings, and config in ${targetEnv.name}. Conversations and customer memory are not copied.`)) return;
    setEnvRefreshing(true);
    setEnvRefreshingId(targetEnv.id);
    setEnvMsg("");
    try {
      const r = await fetch(`/admin/environments/${encodeURIComponent(targetEnv.id)}/refresh`, {
        method: "POST",
        headers: { "content-type": "application/json" },
        body: JSON.stringify({ sourceEnvironmentId: "production" }),
      });
      const j = await r.json();
      if (!r.ok) throw new Error(j.error || r.status);
      setEnvMsg(`Refreshed ${targetEnv.name} — ${j.imported.agents || 0} agents, ${j.imported.knowledge || 0} docs, ${j.imported.learnings || 0} learnings.`);
      setEnvs((xs) => xs.map((e) => e.id === targetEnv.id ? { ...e, lastRefreshedAt: new Date().toISOString() } : e));
    } catch (e) {
      setEnvMsg("Refresh failed: " + e.message);
    } finally {
      setEnvRefreshing(false);
      setEnvRefreshingId(null);
    }
  };

  const envColor = (name) => {
    const n = (name || "").toLowerCase();
    if (n.includes("prod")) return "#22a06b";
    if (n.includes("sandbox") || n.includes("dev")) return "var(--accent)";
    if (n.includes("uat") || n.includes("staging")) return "#d97706";
    if (n.includes("qa") || n.includes("test")) return "#0ea5e9";
    return "var(--ink-soft)";
  };
  const envRel = (iso) => {
    if (!iso) return "";
    const min = Math.floor((Date.now() - new Date(iso).getTime()) / 60000);
    if (min < 1) return "just now";
    if (min < 60) return `${min}m ago`;
    const hr = Math.floor(min / 60);
    if (hr < 24) return `${hr}h ago`;
    return `${Math.floor(hr / 24)}d ago`;
  };
  const activeEnv = envs.find((e) => e.id === activeEnvId) || null;

  useEffect(() => {
    if (!SERVER_URL_NOTIF) return;
    fetch(`${SERVER_URL_NOTIF}/config`)
      .then((r) => r.ok ? r.json() : null)
      .then((c) => {
        if (!c) return;
        if (c.leadManagerEmail) {
          setLeadEmail(c.leadManagerEmail);
          setLeadDraft(c.leadManagerEmail);
          setLeadVerifiedAt(c.leadManagerVerifiedAt || null);
          setLeadPhase(c.leadManagerVerifiedAt ? "verified" : "idle");
        }
        if (c.conflictResolutionMode) setConflictMode(c.conflictResolutionMode);
        if (c.memoryExtractionMode)   setMemoryMode(c.memoryExtractionMode);
        if (c.memoryExtractionInterval != null) setMemoryInterval(c.memoryExtractionInterval);
        if (c.vipFactThreshold != null)        setVipFactThreshold(c.vipFactThreshold);
        if (c.memoryDecayDays != null)          setMemoryDecayDays(c.memoryDecayDays);
        if (c.memoryPrecedence != null)         setMemoryPrecedence(c.memoryPrecedence);
        if (c.memoryConflictDetection != null)  setMemoryConflictDet(c.memoryConflictDetection);
      })
      .catch(() => {});
  }, []);

  const saveConflictMode = (mode) => {
    if (!SERVER_URL_NOTIF) return;
    setConflictMode(mode);
    setConflictSaving(true);
    fetch(`${SERVER_URL_NOTIF}/config`, {
      method: "PATCH",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ conflictResolutionMode: mode }),
    })
      .then(() => window.toast && window.toast(
        mode === "auto"
          ? "Self-learning on — conflicts will be auto-resolved"
          : "Manual mode on — you'll review conflicts yourself",
        "good",
      ))
      .catch(() => window.toast && window.toast("Couldn't save setting", "warn"))
      .finally(() => setConflictSaving(false));
  };

  const saveMemorySettings = (patch) => {
    if (!SERVER_URL_NOTIF) return;
    setMemorySaving(true);
    fetch(`${SERVER_URL_NOTIF}/config`, {
      method: "PATCH",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(patch),
    })
      .then(() => window.toast && window.toast("Memory settings saved", "good"))
      .catch(() => window.toast && window.toast("Couldn't save setting", "warn"))
      .finally(() => setMemorySaving(false));
  };

  const sendLeadCode = () => {
    setLeadErr("");
    const e = leadDraft.trim().toLowerCase();
    if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(e)) {
      setLeadErr("That doesn't look like a valid email.");
      return;
    }
    if (!SERVER_URL_NOTIF) { setLeadErr("Runtime server not connected."); return; }
    setLeadPhase("sending");
    fetch(`${SERVER_URL_NOTIF}/config/email/start`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ email: e }),
    }).then(async (r) => {
      if (!r.ok) {
        const j = await r.json().catch(() => ({}));
        throw new Error(j.error || `status ${r.status}`);
      }
      setLeadPhase("code-sent");
      setLeadCode("");
      window.toast && window.toast(`Verification code sent to ${e}`, "good");
    }).catch((err) => {
      setLeadPhase("idle");
      setLeadErr(String(err.message || err));
    });
  };

  const verifyLeadCode = () => {
    setLeadErr("");
    const e = leadDraft.trim().toLowerCase();
    if (!/^\d{6}$/.test(leadCode)) { setLeadErr("Enter the 6-digit code from the email."); return; }
    setLeadPhase("verifying");
    fetch(`${SERVER_URL_NOTIF}/config/email/verify`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ email: e, code: leadCode }),
    }).then(async (r) => {
      const j = await r.json().catch(() => ({}));
      if (!r.ok) throw new Error(j.error || `status ${r.status}`);
      setLeadEmail(e);
      setLeadVerifiedAt(j.verifiedAt);
      setLeadPhase("verified");
      window.toast && window.toast(`✓  ${e} verified — wrap-up emails will go here`, "good");
    }).catch((err) => {
      setLeadPhase("code-sent");
      setLeadErr(String(err.message || err));
    });
  };

  const changeLeadEmail = () => {
    setLeadPhase("idle");
    setLeadCode("");
    setLeadErr("");
    setLeadDraft("");
  };

  const clearLeadEmail = () => {
    if (!SERVER_URL_NOTIF) return;
    fetch(`${SERVER_URL_NOTIF}/config`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ leadManagerEmail: null }),
    }).then(() => {
      setLeadEmail("");
      setLeadDraft("");
      setLeadVerifiedAt(null);
      setLeadPhase("idle");
      window.toast && window.toast("Wrap-up emails turned off", "warn");
    }).catch(() => {});
  };

  // Profile + voice are controlled and persisted in localStorage so values
  // survive a refresh — that's the closest thing this no-backend demo has
  // to "saving".
  const loadJson = (k, fallback) => {
    try { const v = localStorage.getItem(k); return v ? JSON.parse(v) : fallback; }
    catch { return fallback; }
  };

  // Push profile/voice to the server so they survive across browsers/machines.
  const _surl = () => (typeof window !== "undefined" && window.CITRUS_CONFIG && window.CITRUS_CONFIG.SERVER_URL) || "";
  const patchServerConfig = (payload) => {
    const url = _surl();
    if (!url) return;
    fetch(`${url}/config`, {
      method: "PATCH",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(payload),
    }).catch(() => {});
  };
  const PROFILE_DEFAULT = {
    name: "",
    industry: "Other",
    tz: "London · UTC+0/+1",
    desc: "",
    color: "#E85D1A",
    logo: null,
    website: "",
  };
  const BRAND_COLORS = [
    "#E85D1A", "#2DAF6B", "#3B7CFF", "#B4488C",
    "#D9A23C", "#7A5BCF", "#1A120B", "#C95A4F",
  ];
  const VOICE_DEFAULT = {
    style: "",
    avoid: "",
  };
  const [profile, setProfile] = useState(() => ({ ...PROFILE_DEFAULT, ...loadJson("citrus_profile", {}) }));
  const [voice, setVoice]     = useState(() => loadJson("citrus_voice",   VOICE_DEFAULT));

  // On mount: hydrate profile/voice from server (wins over localStorage so
  // settings stay in sync across browsers / machines when a DB is connected).
  useEffect(() => {
    const url = _surl();
    if (!url) return;
    fetch(`${url}/config`)
      .then((r) => r.ok ? r.json() : null)
      .then((data) => {
        if (!data) return;
        if (data.profile) {
          // Replace, don't merge — see sidebar.jsx self-sync for the rationale.
          // The server is the source of truth; stale localStorage from a
          // previous tenant must not bleed across the swap.
          setProfile(() => ({ ...PROFILE_DEFAULT, ...data.profile }));
          localStorage.setItem("citrus_profile", JSON.stringify(data.profile));
          window.dispatchEvent(new Event("citrus-brand"));
        } else {
          // Tenant has no profile yet — wipe cached one.
          setProfile(PROFILE_DEFAULT);
          localStorage.removeItem("citrus_profile");
          window.dispatchEvent(new Event("citrus-brand"));
        }
        if (data.voice) {
          setVoice((v) => ({ ...v, ...data.voice }));
          localStorage.setItem("citrus_voice", JSON.stringify({ ...loadJson("citrus_voice", VOICE_DEFAULT), ...data.voice }));
        }
      })
      .catch(() => {});
  }, []);
  const logoRef = useRef(null);
  const onLogoFile = (e) => {
    const f = e.target.files && e.target.files[0];
    if (!f) return;
    if (f.size > 1024 * 1024) { window.toast("Logo must be under 1 MB", "warn"); return; }
    const reader = new FileReader();
    reader.onload = () => {
      const next = { ...profile, logo: reader.result };
      setProfile(next);
      localStorage.setItem("citrus_profile", JSON.stringify(next));
      patchServerConfig({ profile: next });
      window.dispatchEvent(new Event("citrus-brand"));
      window.toast("Logo uploaded", "good");
    };
    reader.readAsDataURL(f);
  };
  const removeLogo = () => {
    const next = { ...profile, logo: null };
    setProfile(next);
    localStorage.setItem("citrus_profile", JSON.stringify(next));
    patchServerConfig({ profile: next });
    window.dispatchEvent(new Event("citrus-brand"));
  };
  const setBrandColor = (c) => {
    const next = { ...profile, color: c };
    setProfile(next);
    localStorage.setItem("citrus_profile", JSON.stringify(next));
    patchServerConfig({ profile: next });
    window.dispatchEvent(new Event("citrus-brand"));
  };

  // Website analysis. Uses Claude when an API key is set, otherwise falls
  // back to URL/page heuristics. Key resolution order:
  //   1) window.CITRUS_CONFIG.CLAUDE_API_KEY  (from .env via scripts/dev.sh)
  //   2) localStorage citrus_anthropic_key    (entered in this UI)
  // Sent direct to api.anthropic.com (Anthropic supports browser calls with a header flag).
  const [analyzing, setAnalyzing] = useState(false);
  const [analysis, setAnalysis]   = useState(null);
  const envKey = (typeof window !== "undefined" && window.CITRUS_CONFIG && window.CITRUS_CONFIG.CLAUDE_API_KEY) || "";
  const [apiKey, setApiKey]       = useState(() => {
    if (envKey) return envKey;
    try { return localStorage.getItem("citrus_anthropic_key") || ""; } catch { return ""; }
  });
  const [showKey, setShowKey]     = useState(false);
  const [keyDraft, setKeyDraft]   = useState("");
  const saveKey = () => {
    const trimmed = keyDraft.trim();
    if (!trimmed) return;
    localStorage.setItem("citrus_anthropic_key", trimmed);
    setApiKey(trimmed);
    setKeyDraft("");
    setShowKey(false);
    window.toast("Claude connected — analysis will use it", "good");
  };
  const clearKey = () => {
    localStorage.removeItem("citrus_anthropic_key");
    setApiKey("");
    window.toast("Claude key removed — falling back to heuristics", "warn");
  };
  const runAnalyze = async () => {
    if (!profile.website.trim()) { window.toast("Add your website URL first", "warn"); return; }
    setAnalyzing(true);
    setAnalysis(null);
    try {
      const result = await analyzeWebsite(profile.website);
      if (!result) { window.toast("That doesn't look like a valid URL", "warn"); return; }
      setAnalysis(result);
      if (!result.sourcedFromSite) {
        window.toast("Couldn't read the site directly — used URL hints instead", "warn");
      }
    } catch (e) {
      window.toast("Couldn't read your site — try again", "warn");
    } finally {
      setAnalyzing(false);
    }
  };
  const applyAnalysis = () => {
    if (!analysis) return;
    const next = { ...profile };
    if (analysis.companyName) next.name = analysis.companyName;
    if (analysis.industry && INDUSTRIES.includes(analysis.industry)) next.industry = analysis.industry;
    if (analysis.tz && ALL_TIMEZONES.includes(analysis.tz)) next.tz = analysis.tz;
    // Build a clean description from what we read.
    const base = analysis.description || analysis.summary;
    const sigText = analysis.signals.length
      ? ` Detected on the site: ${analysis.signals.map((s) => s.label).join(", ")}.`
      : "";
    next.desc = base.replace(/\s+$/, "") + sigText;
    // Persist signals so the recommendations stay evidence-based even
    // after the analysis panel is dismissed.
    next.signals = analysis.signals;
    setProfile(next);
    localStorage.setItem("citrus_profile", JSON.stringify(next));
    patchServerConfig({ profile: next });
    window.dispatchEvent(new Event("citrus-brand"));
    window.toast("Profile updated from your site", "good");
    setAnalysis(null);
  };
  const saveProfile = () => {
    localStorage.setItem("citrus_profile", JSON.stringify(profile));
    // Save profile AND mirror the name into per-tenant branding.displayName.
    // The sidebar / topbar / page <title> all read CITRUS_BRANDING.displayName
    // (server-injected on a per-tenant basis); without this mirror the rail
    // keeps showing the default "Citrus" or the previous tenant's name even
    // after the new Owner sets their company name here.
    patchServerConfig({
      profile,
      branding: profile.name ? { displayName: profile.name } : undefined,
    });
    window.dispatchEvent(new Event("citrus-brand"));
    window.toast("Company profile saved", "good");
  };
  const saveVoice   = () => {
    localStorage.setItem("citrus_voice", JSON.stringify(voice));
    patchServerConfig({ voice });
    window.toast("Voice & rules saved", "good");
  };

  // API keys: reveal toggle + last-rotated stamp + add a new key.
  const [keys, setKeys] = useState([
    { id: "k-prod", label: "Production", value: "citr_live_4f9c2e8a1b3d7f6e3a9f", created: "Created Aug 12", used: "last used 4 min ago", revealed: false },
    { id: "k-test", label: "Test mode",  value: "citr_test_8b1e3d6f5a2c9e0d8d12", created: "Created Aug 12", used: "last used 2 days ago", revealed: false },
  ]);
  const toggleReveal = (id) => setKeys((ks) => ks.map((k) => k.id === id ? { ...k, revealed: !k.revealed } : k));
  const rotateKey = (id) => {
    const fresh = "citr_" + Math.random().toString(36).slice(2, 6) + "_" + Math.random().toString(36).slice(2, 22);
    setKeys((ks) => ks.map((k) => k.id === id ? { ...k, value: fresh, used: "just rotated" } : k));
    window.toast("Key rotated — old key revoked", "good");
  };
  const addKey = () => {
    const id = "k-" + Math.random().toString(36).slice(2, 6);
    const fresh = "citr_live_" + Math.random().toString(36).slice(2, 22);
    setKeys((ks) => [...ks, { id, label: "New key", value: fresh, created: "Created just now", used: "never used", revealed: true }]);
    window.toast("New API key created", "good");
  };
  const mask = (v) => v.slice(0, 10) + "•".repeat(18) + v.slice(-4);

  // Danger zone: simple confirm gate before any destructive toast.
  const pauseAll = () => { if (window.confirm("Pause every agent? You can resume any time.")) window.toast("All agents paused", "warn"); };
  // GET /export streams a single JSON file with everything this tenant owns
  // (agents, conversations, knowledge, learnings, profiles, approvals, etc.).
  // Secrets are redacted server-side, other tenants' data is filtered out.
  // We fetch with the Authorization header (browsers can't attach it to a
  // plain navigation), then synthesize a download via an <a download> click.
  const exportAll = async () => {
    try {
      const cfg = (typeof window !== "undefined" && window.CITRUS_CONFIG) || {};
      const serverUrl = (cfg.SERVER_URL || "").replace(/\/$/, "");
      const token = localStorage.getItem("citrus_auth_token") || cfg.DASHBOARD_SECRET || "";
      const res = await fetch(`${serverUrl}/export`, {
        headers: token ? { Authorization: `Bearer ${token}` } : {},
      });
      if (!res.ok) {
        const body = await res.json().catch(() => ({}));
        window.toast(body.error || `Export failed (${res.status})`, "bad");
        return;
      }
      const blob = await res.blob();
      // Pull filename from Content-Disposition; fall back to a sensible default.
      const cd   = res.headers.get("content-disposition") || "";
      const m    = cd.match(/filename="?([^"]+)"?/i);
      const name = (m && m[1]) || `citrus-export-${new Date().toISOString().slice(0, 10)}.json`;
      const url  = URL.createObjectURL(blob);
      const a    = document.createElement("a");
      a.href     = url;
      a.download = name;
      document.body.appendChild(a);
      a.click();
      a.remove();
      setTimeout(() => URL.revokeObjectURL(url), 1000);
      window.toast("Your data is downloading", "good");
    } catch (e) {
      window.toast(`Export failed: ${e.message}`, "bad");
    }
  };
  const deleteWs  = () => { if (window.confirm("Delete workspace? This cannot be undone.")) window.toast("Workspace deletion scheduled — check email to confirm", "bad"); };

  return (
    <div className="st-page">
      <div className="st-side">
        {sections.map(([k, l]) => (
          <button key={k} className={`st-side-i ${section === k ? "is-on" : ""}`} onClick={() => setSection(k)}>{l}</button>
        ))}
      </div>
      <div className="st-pane">
        {section === "profile" ? (
          <>
            <h3 className="st-pane-h">Company profile</h3>

            <div className="st-brand">
              <div className="st-brand-logo" style={profile.color ? { borderColor: profile.color } : null}>
                {profile.logo
                  ? <img src={profile.logo} alt="logo" />
                  : <span className="st-brand-glyph" style={{ background: profile.color || "#E85D1A" }}>{(profile.name || "C")[0]}</span>}
              </div>
              <div className="st-brand-actions">
                <input ref={logoRef} type="file" accept="image/*" onChange={onLogoFile} style={{ display: "none" }} />
                <button type="button" className="btn btn-ghost btn-sm" onClick={() => logoRef.current && logoRef.current.click()}>
                  {profile.logo ? "Replace logo" : "Upload logo"}
                </button>
                {profile.logo ? <button type="button" className="btn btn-ghost btn-sm" onClick={removeLogo}>Remove</button> : null}
                <div className="st-brand-color-l">Brand color</div>
                <div className="st-brand-swatches">
                  {BRAND_COLORS.map((c) => (
                    <button
                      key={c}
                      type="button"
                      className={`st-brand-swatch ${profile.color === c ? "is-on" : ""}`}
                      style={{ background: c }}
                      onClick={() => setBrandColor(c)}
                      aria-label={`Brand color ${c}`}
                    />
                  ))}
                </div>
              </div>
            </div>

            <div className="st-fld">
              <label>Company name</label>
              <input value={profile.name} onChange={(e) => setProfile({ ...profile, name: e.target.value })} />
            </div>

            <div className="st-fld st-website">
              <label>Website</label>
              <div className="st-website-row">
                <input
                  className="st-website-input"
                  placeholder="https://example.com"
                  value={profile.website}
                  onChange={(e) => setProfile({ ...profile, website: e.target.value })}
                  onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); runAnalyze(); } }}
                  disabled={analyzing}
                />
                <button
                  type="button"
                  className="btn btn-ghost btn-sm st-website-btn"
                  onClick={runAnalyze}
                  disabled={analyzing || !profile.website.trim()}
                >
                  {analyzing ? <><span className="st-website-spin" /> Reading…</> : "Analyze site"}
                </button>
              </div>
              <span className="st-fld-hint">
                Paste your website and we'll read it — pulling your company name, industry, time zone, and capabilities to recommend the right agents.
                {apiKey
                  ? <> <strong className="st-website-claude-on">Claude is connected{envKey ? " via .env" : ""}</strong> — analysis will be evidence-based and specific to your site.</>
                  : <> For Claude-quality analysis, <button type="button" className="st-website-link" onClick={() => setShowKey(true)}>connect your Anthropic API key</button> below, or set <code className="st-keyrow-mask">CLAUDE_API_KEY</code> in <code className="st-keyrow-mask">.env</code> and run <code className="st-keyrow-mask">./scripts/dev.sh</code>. Without one, we use best-effort URL/page heuristics.</>}
              </span>

              {(showKey || (!apiKey && false)) ? (
                <div className="st-keyrow">
                  <input
                    className="st-website-input st-keyrow-input"
                    type="password"
                    autoComplete="off"
                    placeholder="sk-ant-..."
                    value={keyDraft}
                    onChange={(e) => setKeyDraft(e.target.value)}
                    onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); saveKey(); } }}
                    autoFocus
                  />
                  <button type="button" className="btn btn-primary btn-sm" onClick={saveKey} disabled={!keyDraft.trim()}>Save key</button>
                  <button type="button" className="btn btn-ghost btn-sm" onClick={() => { setShowKey(false); setKeyDraft(""); }}>Cancel</button>
                </div>
              ) : null}
              {apiKey && !showKey ? (
                <div className="st-keyrow st-keyrow-set">
                  <span className="st-keyrow-meta">Claude key saved · <code className="st-keyrow-mask">{apiKey.slice(0, 7)}…{apiKey.slice(-4)}</code></span>
                  <button type="button" className="btn btn-ghost btn-sm" onClick={() => setShowKey(true)}>Replace</button>
                  <button type="button" className="btn btn-ghost btn-sm" onClick={clearKey}>Remove</button>
                </div>
              ) : null}

              {analysis ? (
                <div className="st-analysis">
                  <div className="st-analysis-head">
                    <span className={`st-analysis-tag ${analysis.sourcedFromSite ? "" : "is-fallback"}`}>
                      {analysis.poweredBy === "claude"
                        ? "analyzed by Claude"
                        : analysis.sourcedFromSite ? "we read your site" : "URL hint only"}
                    </span>
                    <span className="st-analysis-host">{analysis.host}</span>
                  </div>
                  {analysis.title ? (
                    <div className="st-analysis-title">{analysis.title}</div>
                  ) : null}
                  <p className="st-analysis-sum">{analysis.summary}</p>

                  {/* Claude-only enrichment: products, notable projects, leadership */}
                  {analysis.products && analysis.products.length ? (
                    <div className="st-analysis-section">
                      <div className="st-analysis-section-h">Products & services</div>
                      <ul className="st-analysis-list">
                        {analysis.products.slice(0, 8).map((p, i) => <li key={i}>{p}</li>)}
                      </ul>
                    </div>
                  ) : null}
                  {analysis.notableProjects && analysis.notableProjects.length ? (
                    <div className="st-analysis-section">
                      <div className="st-analysis-section-h">Notable projects</div>
                      <ul className="st-analysis-list">
                        {analysis.notableProjects.slice(0, 6).map((p, i) => <li key={i}>{p}</li>)}
                      </ul>
                    </div>
                  ) : null}
                  {analysis.leadership && analysis.leadership.length ? (
                    <div className="st-analysis-section">
                      <div className="st-analysis-section-h">Leadership</div>
                      <ul className="st-analysis-list">
                        {analysis.leadership.slice(0, 6).map((p, i) => <li key={i}>{p}</li>)}
                      </ul>
                    </div>
                  ) : null}
                  {analysis.location ? (
                    <div className="st-analysis-section">
                      <div className="st-analysis-section-h">Location</div>
                      <div className="st-analysis-loc">{analysis.location}</div>
                    </div>
                  ) : null}

                  {analysis.signals.length ? (
                    <div className="st-analysis-signals">
                      <div className="st-analysis-signals-l">What we found on your site</div>
                      <div className="st-analysis-signals-list">
                        {analysis.signals.map((s) => (
                          <span key={s.id} className="st-analysis-chip">
                            <span className="st-analysis-chip-l">{s.label}</span>
                            {s.snippet ? <span className="st-analysis-chip-q">"{s.snippet.slice(0, 38)}"</span> : null}
                          </span>
                        ))}
                      </div>
                    </div>
                  ) : null}
                  <div className="st-analysis-detect">
                    {analysis.companyName ? (
                      <div className="st-analysis-meta">
                        <span className="st-analysis-meta-l">Company</span>
                        <strong>{analysis.companyName}</strong>
                      </div>
                    ) : null}
                    {analysis.industry ? (
                      <div className="st-analysis-meta">
                        <span className="st-analysis-meta-l">Industry</span>
                        <strong>{analysis.industry}</strong>
                      </div>
                    ) : null}
                    {analysis.tz ? (
                      <div className="st-analysis-meta">
                        <span className="st-analysis-meta-l">Time zone</span>
                        <strong>{analysis.tz}</strong>
                      </div>
                    ) : null}
                  </div>
                  {!analysis.sourcedFromSite ? (
                    <div className="st-analysis-warn">
                      We couldn't reach your site (it may block proxies). Showing best-guess hints from the URL — paste a description below for sharper recommendations.
                    </div>
                  ) : analysis.signals.length === 0 ? (
                    <div className="st-analysis-warn">
                      We reached your site but didn't find enough recognizable signal. Paste 1–2 sentences below describing what you do — that'll sharpen the recommendations.
                    </div>
                  ) : null}
                  <div className="st-analysis-cta">
                    <button type="button" className="btn btn-primary btn-sm" onClick={applyAnalysis}>
                      Use this
                    </button>
                    <button type="button" className="btn btn-ghost btn-sm" onClick={() => setAnalysis(null)}>
                      Dismiss
                    </button>
                  </div>
                </div>
              ) : null}
            </div>

            <div className="st-fld-row">
              <div className="st-fld">
                <label>Industry</label>
                <select
                  className="st-select"
                  value={INDUSTRIES.includes(profile.industry) ? profile.industry : "Other"}
                  onChange={(e) => setProfile({ ...profile, industry: e.target.value })}
                >
                  {INDUSTRIES.map((i) => <option key={i} value={i}>{i}</option>)}
                </select>
              </div>
              <div className="st-fld">
                <label>Time zone</label>
                <select
                  className="st-select"
                  value={normalizeTz(profile.tz)}
                  onChange={(e) => setProfile({ ...profile, tz: e.target.value })}
                >
                  {TIMEZONE_GROUPS.map(([group, zones]) => (
                    <optgroup key={group} label={group}>
                      {zones.map((z) => <option key={z} value={z}>{z}</option>)}
                    </optgroup>
                  ))}
                </select>
              </div>
            </div>
            <div className="st-fld">
              <label>What identifies the business in one sentence</label>
              <textarea value={profile.desc} onChange={(e) => setProfile({ ...profile, desc: e.target.value })} rows={3} />
              <span className="st-fld-hint">Your agents read this when they introduce themselves — and we use it to recommend the right hires below.</span>
            </div>

            {/* ---- Onboarding-captured fields (editable, surfaced here so the
                   owner can revise without re-running the wizard) ---- */}
            <div className="st-fld">
              <label>Who are your customers</label>
              <textarea value={profile.customers || ""}
                onChange={(e) => setProfile({ ...profile, customers: e.target.value })} rows={2}
                placeholder='"Procurement managers at mid-sized factories in Cairo and Alexandria."' />
              <span className="st-fld-hint">One sentence. Used by every agent when qualifying or addressing leads.</span>
            </div>

            <div className="st-fld">
              <label>Languages your agents speak</label>
              <div className="ob-chips" style={{ marginTop: 4 }}>
                {["English","Arabic","Spanish","French","Portuguese","German","Hindi","Mandarin","Turkish","Russian","Indonesian"].map((l) => {
                  const on = (profile.languages || []).includes(l);
                  return (
                    <button key={l} type="button"
                      className={`ob-chip ${on ? "is-on" : ""}`}
                      onClick={() => setProfile({ ...profile, languages: on
                        ? (profile.languages || []).filter((x) => x !== l)
                        : [...(profile.languages || []), l] })}>
                      {l}
                    </button>
                  );
                })}
              </div>
            </div>

            <div className="st-fld">
              <label>Where customers reach you</label>
              <div className="ob-chips" style={{ marginTop: 4 }}>
                {[
                  { id: "whatsapp",  label: "WhatsApp" },
                  { id: "telegram",  label: "Telegram" },
                  { id: "email",     label: "Email" },
                  { id: "instagram", label: "Instagram DMs" },
                  { id: "webchat",   label: "Website chat" },
                  { id: "sms",       label: "SMS" },
                ].map((c) => {
                  const on = (profile.channels || []).includes(c.id);
                  return (
                    <button key={c.id} type="button"
                      className={`ob-chip ${on ? "is-on" : ""}`}
                      onClick={() => setProfile({ ...profile, channels: on
                        ? (profile.channels || []).filter((x) => x !== c.id)
                        : [...(profile.channels || []), c.id] })}>
                      {c.label}
                    </button>
                  );
                })}
              </div>
              <span className="st-fld-hint">Agents only work on channels listed here. Add more once you've connected them.</span>
            </div>

            <div style={{ display: "flex", gap: 8, alignItems: "center", marginTop: 4 }}>
              <button className="btn btn-primary btn-sm" onClick={saveProfile}>Save changes</button>
              <button
                className="btn btn-ghost btn-sm"
                onClick={() => {
                  // Per-user dismissal flag is what gates the auto-open;
                  // clearing it lets the wizard fire on the next mount.
                  // To re-run NOW, dispatch the citrus event the App listens for.
                  try {
                    const me = window.__citrusUser || {};
                    localStorage.removeItem(`citrus_onboarding_dismissed:${me.email || "anon"}`);
                  } catch {}
                  if (typeof window.dispatchEvent === "function") {
                    window.dispatchEvent(new Event("citrus-open-onboarding"));
                  }
                }}
                title="Re-open the six-step intake. Your current answers stay until you save the new ones.">
                Re-run onboarding
              </button>
            </div>

            {/* Tailored agent recommendations based on industry + description */}
            <RecommendationsPanel
              profile={profile}
              companyName={profile.name}
              signals={analysis ? analysis.signals : profile.signals}
              onHire={(rec) => onNew && onNew({ flow: rec.flow, name: rec.name.replace(/[^a-zA-Z0-9]/g, "").slice(0, 8).toUpperCase() })}
            />
          </>
        ) : section === "voice" ? (
          <>
            <h3 className="st-pane-h">How your agents speak</h3>
            <div className="st-fld">
              <label>House style</label>
              <textarea value={voice.style} onChange={(e) => setVoice({ ...voice, style: e.target.value })} rows={5} />
            </div>
            <div className="st-fld">
              <label>Things to never say</label>
              <textarea value={voice.avoid} onChange={(e) => setVoice({ ...voice, avoid: e.target.value })} rows={4} />
            </div>
            <button className="btn btn-primary btn-sm" onClick={saveVoice}>Save</button>
          </>
        ) : section === "learning" ? (
          <>
            <h3 className="st-pane-h">Learning behaviour</h3>
            <p className="st-pane-s">
              Agents learn from every conversation. When two lessons contradict each other, a conflict is detected. Choose how those conflicts get resolved.
            </p>
            <div className="st-learn-options">
              <label className={`st-learn-opt${conflictMode === "manual" ? " is-on" : ""}`}>
                <input
                  type="radio"
                  name="conflictMode"
                  value="manual"
                  checked={conflictMode === "manual"}
                  disabled={conflictSaving}
                  onChange={() => saveConflictMode("manual")}
                />
                <div className="st-learn-opt-body">
                  <div className="st-learn-opt-t">Manual</div>
                  <div className="st-learn-opt-s">
                    Conflicts appear as cards on the Knowledge page and on each agent's detail page. You pick which lesson to keep.
                  </div>
                </div>
              </label>
              <label className={`st-learn-opt${conflictMode === "auto" ? " is-on" : ""}`}>
                <input
                  type="radio"
                  name="conflictMode"
                  value="auto"
                  checked={conflictMode === "auto"}
                  disabled={conflictSaving}
                  onChange={() => saveConflictMode("auto")}
                />
                <div className="st-learn-opt-body">
                  <div className="st-learn-opt-t">Self-learning <span className="st-learn-badge-auto">Auto</span></div>
                  <div className="st-learn-opt-s">
                    When a conflict is detected, Claude picks the lesson that best serves your company goals, the agent's role, and the rest of the established knowledge — no manual review needed.
                  </div>
                </div>
              </label>
            </div>
            {conflictMode === "auto" && (
              <p className="st-pane-s" style={{ marginTop: 16 }}>
                Auto mode uses your company profile and voice rules as the decision context. Keep those up to date under <strong>Company</strong> and <strong>Voice &amp; rules</strong> for best results.
              </p>
            )}

            <h3 className="st-pane-h" style={{ marginTop: 32 }}>Customer memory</h3>
            <p className="st-pane-s">
              After each conversation, agents extract a few short facts about the customer (preferences, plan, past issues) and remember them across future conversations.
            </p>
            <div className="st-learn-options">
              <label className={`st-learn-opt${memoryMode === "scheduled" ? " is-on" : ""}`}>
                <input
                  type="radio"
                  name="memoryMode"
                  value="scheduled"
                  checked={memoryMode === "scheduled"}
                  disabled={memorySaving}
                  onChange={() => { setMemoryMode("scheduled"); saveMemorySettings({ memoryExtractionMode: "scheduled" }); }}
                />
                <div className="st-learn-opt-body">
                  <div className="st-learn-opt-t">Scheduled</div>
                  <div className="st-learn-opt-s">Memory is extracted on a background timer. Lower cost — batches extraction across all conversations.</div>
                </div>
              </label>
              <label className={`st-learn-opt${memoryMode === "realtime" ? " is-on" : ""}`}>
                <input
                  type="radio"
                  name="memoryMode"
                  value="realtime"
                  checked={memoryMode === "realtime"}
                  disabled={memorySaving}
                  onChange={() => { setMemoryMode("realtime"); saveMemorySettings({ memoryExtractionMode: "realtime" }); }}
                />
                <div className="st-learn-opt-body">
                  <div className="st-learn-opt-t">Real-time <span className="st-learn-badge-auto">Live</span></div>
                  <div className="st-learn-opt-s">Memory is extracted immediately when a conversation wraps up. Facts are ready for the next message without waiting.</div>
                </div>
              </label>
            </div>

            {memoryMode === "scheduled" && (
              <div className="st-mem-fields">
                <div className="st-fld">
                  <label>Extraction interval (minutes)</label>
                  <div style={{ display: "flex", gap: 8, alignItems: "center" }}>
                    <input
                      className="set-input"
                      type="number"
                      min={1}
                      max={1440}
                      value={memoryInterval}
                      onChange={(e) => setMemoryInterval(Number(e.target.value))}
                      style={{ width: 90 }}
                    />
                    <button
                      className="btn btn-ghost btn-sm"
                      disabled={memorySaving}
                      onClick={() => saveMemorySettings({ memoryExtractionInterval: memoryInterval })}
                    >Save</button>
                  </div>
                </div>
              </div>
            )}

            <div className="st-mem-fields" style={{ marginTop: 16 }}>
              <div className="st-fld">
                <label>VIP brief threshold (facts)</label>
                <div style={{ display: "flex", gap: 8, alignItems: "center" }}>
                  <input
                    className="set-input"
                    type="number"
                    min={1}
                    max={100}
                    value={vipFactThreshold}
                    onChange={(e) => setVipFactThreshold(Number(e.target.value))}
                    style={{ width: 90 }}
                  />
                  <button
                    className="btn btn-ghost btn-sm"
                    disabled={memorySaving}
                    onClick={() => saveMemorySettings({ vipFactThreshold })}
                  >Save</button>
                </div>
                <div className="st-pane-s" style={{ marginTop: 4 }}>Once a customer reaches this many facts, a customer brief is auto-generated and used in the prompt instead of the raw list. Facts are stored without a cap.</div>
              </div>
              <div className="st-fld" style={{ marginTop: 12 }}>
                <label>Fact decay (days)</label>
                <div style={{ display: "flex", gap: 8, alignItems: "center" }}>
                  <input
                    className="set-input"
                    type="number"
                    min={1}
                    max={3650}
                    value={memoryDecayDays}
                    onChange={(e) => setMemoryDecayDays(Number(e.target.value))}
                    style={{ width: 90 }}
                  />
                  <button
                    className="btn btn-ghost btn-sm"
                    disabled={memorySaving}
                    onClick={() => saveMemorySettings({ memoryDecayDays })}
                  >Save</button>
                </div>
                <div className="st-pane-s" style={{ marginTop: 4 }}>Facts older than this are excluded from the agent's context (but kept in storage).</div>
              </div>
            </div>

            <div className="st-mem-toggles" style={{ marginTop: 20, display: "flex", flexDirection: "column", gap: 12 }}>
              <label className="st-mem-toggle-row">
                <input
                  type="checkbox"
                  checked={memoryPrecedence}
                  disabled={memorySaving}
                  onChange={(e) => {
                    setMemoryPrecedence(e.target.checked);
                    saveMemorySettings({ memoryPrecedence: e.target.checked });
                  }}
                />
                <div>
                  <div className="st-mem-toggle-t">Memory overrides general rules per customer</div>
                  <div className="st-pane-s">Tells the agent that customer-specific facts take priority over learned rules for that person only. Example: the rule says "never reply in English" but this customer's memory says "prefers English" — the agent uses English for them.</div>
                </div>
              </label>
              <label className="st-mem-toggle-row">
                <input
                  type="checkbox"
                  checked={memoryConflictDet}
                  disabled={memorySaving}
                  onChange={(e) => {
                    setMemoryConflictDet(e.target.checked);
                    saveMemorySettings({ memoryConflictDetection: e.target.checked });
                  }}
                />
                <div>
                  <div className="st-mem-toggle-t">Flag facts that contradict a learned rule</div>
                  <div className="st-pane-s">When a new fact seems to contradict one of the agent's behavioral rules, it is held for your review instead of being used immediately. Flagged facts appear with a warning in the Memory tab.</div>
                </div>
              </label>
            </div>
          </>
        ) : section === "notifications" ? (
          <>
            <h3 className="st-pane-h">Lead notifications</h3>
            <p className="st-pane-s">
              When a customer conversation wraps up, Citrus emails this address a short recap (name, company, what they wanted, status). Verify the address with a one-time code so we know it's yours.
            </p>

            {leadPhase === "verified" ? (
              <div className="st-key">
                <div className="st-key-body">
                  <div className="st-key-l">✓ Verified</div>
                  <div className="st-key-v">{leadEmail}</div>
                  <div className="st-key-m">
                    {leadVerifiedAt ? `Verified ${new Date(leadVerifiedAt).toLocaleString()}` : "Wrap-up emails go here."}
                  </div>
                </div>
                <button className="btn btn-ghost btn-sm" onClick={changeLeadEmail}>Change</button>
                <button className="btn btn-ghost btn-sm" onClick={clearLeadEmail}>Turn off</button>
              </div>
            ) : leadPhase === "code-sent" || leadPhase === "verifying" ? (
              <>
                <div className="st-fld">
                  <label>Code sent to {leadDraft}</label>
                  <input
                    className="set-input"
                    type="text"
                    inputMode="numeric"
                    maxLength={6}
                    placeholder="6-digit code"
                    autoFocus
                    value={leadCode}
                    onChange={(e) => setLeadCode(e.target.value.replace(/\D/g, "").slice(0, 6))}
                    onKeyDown={(e) => { if (e.key === "Enter") verifyLeadCode(); }}
                  />
                </div>
                <div style={{ display: "flex", gap: 8, alignItems: "center" }}>
                  <button className="btn btn-primary btn-sm" onClick={verifyLeadCode} disabled={leadPhase === "verifying"}>
                    {leadPhase === "verifying" ? "Verifying…" : "Verify"}
                  </button>
                  <button className="btn btn-ghost btn-sm" onClick={sendLeadCode}>Resend code</button>
                  <button className="btn btn-ghost btn-sm" onClick={changeLeadEmail}>Use a different email</button>
                </div>
                {leadErr ? <p className="st-pane-s" style={{ color: "#C95A4F", marginTop: 12 }}>{leadErr}</p> : null}
                <p className="st-pane-s" style={{ marginTop: 16 }}>
                  Check your inbox (and spam folder). The code expires in 10 minutes.
                </p>
              </>
            ) : (
              <>
                <div className="st-fld">
                  <label>Lead manager email</label>
                  <input
                    className="set-input"
                    type="email"
                    placeholder="you@yourcompany.com"
                    value={leadDraft}
                    onChange={(e) => setLeadDraft(e.target.value)}
                    onKeyDown={(e) => { if (e.key === "Enter") sendLeadCode(); }}
                  />
                </div>
                <div style={{ display: "flex", gap: 8, alignItems: "center" }}>
                  <button className="btn btn-primary btn-sm" onClick={sendLeadCode} disabled={leadPhase === "sending"}>
                    {leadPhase === "sending" ? "Sending…" : "Send code"}
                  </button>
                  {leadEmail ? <span className="st-pane-s">Currently set: {leadEmail} {leadVerifiedAt ? "(verified)" : "(unverified)"}</span> : null}
                </div>
                {leadErr ? <p className="st-pane-s" style={{ color: "#C95A4F", marginTop: 12 }}>{leadErr}</p> : null}
              </>
            )}

            <p className="st-pane-s" style={{ marginTop: 24 }}>
              The wrap-up email is sent <strong>from the Google account that deployed your Apps Script</strong>. Set up the Apps Script per <code>server/README.md</code> if you haven't yet.
            </p>

            {/* ---- LINKED GMAIL ---- */}
            <div style={{ marginTop: 32, paddingTop: 24, borderTop: "1px solid var(--border)" }}>
              <h3 className="st-pane-h" style={{ marginBottom: 6 }}>Linked Gmail</h3>
              <p className="st-pane-s" style={{ marginBottom: 16 }}>
                Connect a Gmail account and all outbound mail from this tenant (wrap-up recaps, team invites, verification codes) will be sent <strong>from that address</strong> using the Gmail API — no Apps Script involved. We only request the <code>gmail.send</code> scope: we can send mail on your behalf but cannot read or delete anything.
              </p>

              {gmailLinked === null ? (
                <p className="st-pane-s">Checking status…</p>
              ) : gmailLinked ? (
                <div style={{
                  display: "flex", alignItems: "center", gap: 14,
                  padding: "14px 16px",
                  background: "color-mix(in oklab, #3F8C3A 6%, var(--paper))",
                  border: "1px solid color-mix(in oklab, #3F8C3A 30%, var(--border))",
                  borderRadius: 12,
                }}>
                  <div style={{
                    width: 32, height: 32, borderRadius: "50%",
                    background: "color-mix(in oklab, #3F8C3A 18%, transparent)",
                    display: "grid", placeItems: "center", color: "#3F8C3A", fontSize: 18, fontWeight: 600,
                  }}>✓</div>
                  <div style={{ flex: 1 }}>
                    <div style={{ fontSize: 14, fontWeight: 600 }}>Sending from {gmailEmail || "linked Gmail"}</div>
                    <div style={{ fontSize: 12, color: "var(--ink-2)", marginTop: 2 }}>
                      {gmailConnectedAt ? `Connected ${new Date(gmailConnectedAt).toLocaleDateString()}` : "Connected"}
                    </div>
                  </div>
                  <button className="btn btn-ghost btn-sm" disabled={gmailBusy} onClick={disconnectGmail}>
                    {gmailBusy ? "…" : "Disconnect"}
                  </button>
                </div>
              ) : (
                <button className="btn btn-primary btn-sm" onClick={connectGmail} style={{ display: "inline-flex", alignItems: "center", gap: 8 }}>
                  <svg width="14" height="14" viewBox="0 0 18 18" aria-hidden="true">
                    <path fill="#fff" d="M17.64 9.2c0-.637-.057-1.251-.164-1.84H9v3.481h4.844c-.209 1.125-.843 2.078-1.796 2.717v2.258h2.908c1.702-1.567 2.684-3.874 2.684-6.615z"/>
                    <path fill="#fff" d="M9 18c2.43 0 4.467-.806 5.956-2.18l-2.908-2.259c-.806.54-1.837.86-3.048.86-2.344 0-4.328-1.584-5.036-3.711H.957v2.332A8.997 8.997 0 0 0 9 18z" opacity="0.7"/>
                    <path fill="#fff" d="M3.964 10.71A5.41 5.41 0 0 1 3.682 9c0-.593.102-1.17.282-1.71V4.958H.957A8.996 8.996 0 0 0 0 9c0 1.452.348 2.827.957 4.042l3.007-2.332z" opacity="0.55"/>
                    <path fill="#fff" d="M9 3.58c1.321 0 2.508.454 3.44 1.345l2.582-2.58C13.463.891 11.426 0 9 0A8.997 8.997 0 0 0 .957 4.958L3.964 7.29C4.672 5.163 6.656 3.58 9 3.58z" opacity="0.4"/>
                  </svg>
                  Connect Gmail
                </button>
              )}

              {gmailErr ? <p className="st-pane-s" style={{ color: "#C95A4F", marginTop: 12 }}>{gmailErr}</p> : null}
            </div>
          </>
        ) : section === "model" ? (
          <ModelProviderSection />
        ) : section === "api" ? (
          <>
            <h3 className="st-pane-h">API keys</h3>
            <p className="st-pane-s">Use these if you want to call your agents from your own systems. Treat them like passwords.</p>
            {keys.map((k) => (
              <div key={k.id} className="st-key">
                <div className="st-key-body">
                  <div className="st-key-l">{k.label}</div>
                  <div className="st-key-v">{k.revealed ? k.value : mask(k.value)}</div>
                  <div className="st-key-m">{k.created} · {k.used}</div>
                </div>
                <button className="btn btn-ghost btn-sm" onClick={() => toggleReveal(k.id)}>{k.revealed ? "Hide" : "Reveal"}</button>
                <button className="btn btn-ghost btn-sm" onClick={() => rotateKey(k.id)}>Rotate</button>
              </div>
            ))}
            <button className="btn btn-primary btn-sm" onClick={addKey}>+ New key</button>
          </>
        ) : section === "environments" ? (
          <div className="st-pane">

            {/* Header */}
            <div>
              <h3 className="st-pane-h">Environments</h3>
              <p className="st-pane-s">Switch between Production, UAT, Sandbox, or any named Citrus instance. Switching reloads the dashboard pointed at the selected server.</p>
            </div>

            {/* Active banner */}
            {activeEnv ? (
              <div style={{ display: "flex", alignItems: "center", gap: 16, padding: "16px 20px", background: `color-mix(in oklab, ${envColor(activeEnv.name)} 8%, var(--paper))`, border: `1.5px solid color-mix(in oklab, ${envColor(activeEnv.name)} 30%, var(--border))`, borderRadius: 14 }}>
                <div style={{ width: 11, height: 11, borderRadius: "50%", background: envColor(activeEnv.name), boxShadow: `0 0 0 4px color-mix(in oklab, ${envColor(activeEnv.name)} 22%, transparent)`, flexShrink: 0, animation: "envPulse 2s infinite" }} />
                <div style={{ flex: 1, minWidth: 0 }}>
                  <div style={{ fontSize: 11, fontWeight: 700, letterSpacing: "0.09em", textTransform: "uppercase", color: envColor(activeEnv.name), marginBottom: 3 }}>Active environment</div>
                  <div style={{ fontWeight: 600, fontSize: 15 }}>{activeEnv.name}</div>
                  <div style={{ fontSize: 12, color: "var(--ink-2)", fontFamily: "var(--mono-font)", marginTop: 2, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{activeEnv.url}</div>
                </div>
                <button className="btn btn-ghost btn-sm" onClick={() => switchEnv("")} style={{ flexShrink: 0 }}>Reset to default</button>
              </div>
            ) : (
              <div style={{ display: "flex", alignItems: "center", gap: 16, padding: "16px 20px", background: "color-mix(in oklab, #22a06b 6%, var(--paper))", border: "1.5px solid color-mix(in oklab, #22a06b 25%, var(--border))", borderRadius: 14 }}>
                <div style={{ width: 11, height: 11, borderRadius: "50%", background: "#22a06b", boxShadow: "0 0 0 4px color-mix(in oklab, #22a06b 22%, transparent)", flexShrink: 0 }} />
                <div>
                  <div style={{ fontSize: 11, fontWeight: 700, letterSpacing: "0.09em", textTransform: "uppercase", color: "#22a06b", marginBottom: 3 }}>Active environment</div>
                  <div style={{ fontWeight: 600, fontSize: 15 }}>Default (CITRUS_CONFIG)</div>
                  <div style={{ fontSize: 12, color: "var(--ink-2)", marginTop: 2 }}>Using server configuration from the loaded page</div>
                </div>
              </div>
            )}

            {/* Environment cards */}
            {envs.length > 0 && (
              <div style={{ display: "grid", gap: 8 }}>
                {envs.map((env) => {
                  const isActive = env.id === activeEnvId;
                  const col = envColor(env.name);
                  return (
                    <div key={env.id} style={{ display: "flex", alignItems: "center", gap: 14, padding: "14px 18px 14px 0", background: isActive ? `color-mix(in oklab, ${col} 5%, var(--paper))` : "var(--paper-2)", border: `1px solid ${isActive ? `color-mix(in oklab, ${col} 25%, var(--border))` : "var(--border)"}`, borderRadius: 12, borderLeft: `4px solid ${col}`, paddingLeft: 18, transition: "border-color 0.15s, background 0.15s" }}>
                      <div style={{ flex: 1, minWidth: 0 }}>
                        <div style={{ display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap" }}>
                          <span style={{ fontWeight: 600, fontSize: 14 }}>{env.name}</span>
                          {isActive && (
                            <span style={{ fontSize: 10, fontWeight: 700, letterSpacing: "0.1em", textTransform: "uppercase", color: col, background: `color-mix(in oklab, ${col} 14%, transparent)`, padding: "2px 8px", borderRadius: 999 }}>Active</span>
                          )}
                          {env.secret && (
                            <span style={{ fontSize: 10, color: "var(--ink-2)", background: "color-mix(in oklab, var(--ink) 7%, transparent)", padding: "2px 7px", borderRadius: 999, fontFamily: "var(--mono-font)", letterSpacing: "0.04em" }}>🔑 secured</span>
                          )}
                        </div>
                        <div style={{ fontSize: 12, color: "var(--ink-2)", fontFamily: "var(--mono-font)", marginTop: 4, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{env.url}</div>
                      </div>
                      <div style={{ display: "flex", gap: 6, flexShrink: 0, alignItems: "center" }}>
                        {!isActive && (
                          <button className="btn btn-ghost btn-sm" onClick={() => switchEnv(env.id)}>Switch →</button>
                        )}
                      </div>
                    </div>
                  );
                })}
              </div>
            )}

            {/* Request environment */}
            <div style={{ border: "1.5px dashed var(--border)", borderRadius: 14, padding: "22px 24px" }}>
              <div style={{ fontSize: 11, fontWeight: 700, letterSpacing: "0.1em", textTransform: "uppercase", color: "var(--ink-2)", marginBottom: 14 }}>Request environment</div>
              <div style={{ display: "grid", gridTemplateColumns: "1fr 180px", gap: 10 }}>
                <div className="st-fld">
                  <input
                    className="st-input"
                    placeholder="Name (e.g. Sandbox)"
                    value={envRequestDraft.name}
                    onChange={(e) => setEnvRequestDraft((d) => ({ ...d, name: e.target.value }))}
                    onKeyDown={(e) => e.key === "Enter" && requestEnvironment()}
                  />
                </div>
                <div className="st-fld">
                  <select
                    className="st-input"
                    value={envRequestDraft.type}
                    onChange={(e) => setEnvRequestDraft((d) => ({ ...d, type: e.target.value }))}
                  >
                    <option value="sandbox">Sandbox</option>
                    <option value="uat">UAT</option>
                    <option value="qa">QA</option>
                  </select>
                </div>
                <div className="st-fld" style={{ gridColumn: "1 / -1" }}>
                  <input
                    className="st-input"
                    placeholder="Reason or notes for Citrus (optional)"
                    value={envRequestDraft.reason}
                    onChange={(e) => setEnvRequestDraft((d) => ({ ...d, reason: e.target.value }))}
                    onKeyDown={(e) => e.key === "Enter" && requestEnvironment()}
                  />
                </div>
              </div>
              <button
                className="btn btn-primary btn-sm"
                style={{ marginTop: 14 }}
                onClick={requestEnvironment}
                disabled={!envRequestDraft.name.trim()}
              >
                Request environment
              </button>
            </div>

            {envRequests.length > 0 && (
              <div style={{ display: "grid", gap: 8 }}>
                <div style={{ fontSize: 11, fontWeight: 700, letterSpacing: "0.1em", textTransform: "uppercase", color: "var(--ink-2)" }}>Requests</div>
                {envRequests.slice().reverse().map((r) => (
                  <div key={r.id} style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 12, padding: "12px 14px", background: "var(--paper-2)", border: "1px solid var(--border)", borderRadius: 10 }}>
                    <div>
                      <div style={{ fontSize: 13, fontWeight: 650 }}>{r.requestedName}</div>
                      <div style={{ fontSize: 12, color: "var(--ink-2)", marginTop: 2 }}>{(r.requestedType || "sandbox").toUpperCase()} · requested {envRel(r.createdAt)}</div>
                    </div>
                    <span style={{
                      fontSize: 10,
                      fontWeight: 800,
                      letterSpacing: "0.08em",
                      textTransform: "uppercase",
                      color: r.status === "approved" ? "#22a06b" : r.status === "rejected" ? "#C04545" : "#d97706",
                    }}>{r.status}</span>
                  </div>
                ))}
              </div>
            )}

            {/* Sandbox refresh */}
            {envs.some((e) => e.type !== "production") && (
              <div style={{ background: "color-mix(in oklab, var(--ink) 3%, var(--paper))", border: "1px solid var(--border)", borderRadius: 14, padding: "22px 24px" }}>
                <div style={{ display: "flex", alignItems: "flex-start", gap: 14, marginBottom: 16 }}>
                  <div style={{ width: 36, height: 36, borderRadius: 10, background: "color-mix(in oklab, var(--accent) 12%, var(--paper))", border: "1px solid color-mix(in oklab, var(--accent) 20%, var(--border))", display: "grid", placeItems: "center", fontSize: 16, flexShrink: 0 }}>↓</div>
                  <div>
                    <div style={{ fontWeight: 600, fontSize: 14, marginBottom: 4 }}>Sandbox refresh</div>
                    <div style={{ fontSize: 13, color: "var(--ink-2)", lineHeight: 1.5 }}>Copy agents, knowledge, learnings, and config from Production into an approved sandbox. Conversations and customer memory are not copied.</div>
                  </div>
                </div>
                <div style={{ display: "grid", gap: 8 }}>
                  {envs.filter((e) => e.type !== "production").map((env) => {
                    const pointsAtPrimary = envPointsAtPrimary(env);
                    return (
                      <div key={env.id} style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 12, padding: "10px 12px", background: "var(--paper)", border: "1px solid var(--border)", borderRadius: 10 }}>
                        <div style={{ minWidth: 0 }}>
                          <div style={{ fontSize: 13, fontWeight: 650 }}>{env.name}</div>
                          <div style={{ fontSize: 11, color: "var(--ink-2)", marginTop: 2 }}>
                            {pointsAtPrimary ? "Awaiting separate sandbox runtime" : env.lastRefreshedAt ? `Last refreshed ${envRel(env.lastRefreshedAt)}` : "Not refreshed yet"}
                          </div>
                        </div>
                        <button
                          className="btn btn-primary btn-sm"
                          onClick={() => doRefresh(env)}
                          disabled={envRefreshing || (env.status || "active") !== "active" || pointsAtPrimary}
                          title={pointsAtPrimary ? "Citrus must attach a separate sandbox runtime first." : ""}
                          style={{ flexShrink: 0 }}
                        >
                          {envRefreshingId === env.id ? "Refreshing…" : "Refresh from Production"}
                        </button>
                      </div>
                    );
                  })}
                </div>
                {envMsg && (
                  <div style={{ marginTop: 12, padding: "9px 14px", borderRadius: 8, fontSize: 13, fontWeight: 500, background: envMsg.startsWith("Refresh failed") ? "color-mix(in oklab, #C04545 8%, transparent)" : "color-mix(in oklab, #22a06b 8%, transparent)", color: envMsg.startsWith("Refresh failed") ? "#C04545" : "#22a06b", border: `1px solid ${envMsg.startsWith("Refresh failed") ? "color-mix(in oklab, #C04545 20%, transparent)" : "color-mix(in oklab, #22a06b 20%, transparent)"}` }}>
                    {envMsg}
                  </div>
                )}
              </div>
            )}
            <style>{`@keyframes envPulse { 0%,100% { box-shadow: 0 0 0 4px color-mix(in oklab, currentColor 22%, transparent); } 50% { box-shadow: 0 0 0 7px color-mix(in oklab, currentColor 8%, transparent); } }`}</style>
          </div>
        ) : section === "invoices" ? (
          <>
            <h3 className="st-pane-h">Invoices</h3>
            <div className="kn-empty">No invoices yet — your first one will appear here on the 1st of next month.</div>
          </>
        ) : (
          <>
            <h3 className="st-pane-h">Danger zone</h3>
            <div className="st-danger">
              <div className="st-danger-body">
                <div className="st-danger-t">Pause all agents</div>
                <div className="st-danger-s">Stops every agent immediately. You can resume any time.</div>
              </div>
              <button className="btn btn-ghost btn-sm" onClick={pauseAll}>Pause all</button>
            </div>
            <div className="st-danger">
              <div className="st-danger-body">
                <div className="st-danger-t">Export all my data</div>
                <div className="st-danger-s">Download a single JSON file with every agent, conversation, document, learning, profile, and decision in your workspace. Sensitive values (access tokens, secrets) are redacted; other tenants&apos; data is never included.</div>
              </div>
              <button className="btn btn-ghost btn-sm" onClick={exportAll}>Export all my data</button>
            </div>
            <div className="st-danger st-danger-red">
              <div className="st-danger-body">
                <div className="st-danger-t">Delete workspace</div>
                <div className="st-danger-s">Permanently deletes all agents, history, and data. This can't be undone.</div>
              </div>
              <button className="btn btn-ghost btn-sm st-danger-btn" onClick={deleteWs}>Delete</button>
            </div>
          </>
        )}
      </div>
    </div>
  );
}

// ============ CLAUDE-POWERED RECOMMENDATIONS ============
// The heuristic engine above ranks 15 generic archetypes. That's fine as a
// fallback, but the recs read like stock answers ("WhatsApp Helper",
// "Lead Grabber") that look identical for every business. When a Claude
// API key is configured, we ask the model to invent agents named for what
// they'd actually do for THIS specific business — referencing their
// products, projects, country, and detected on-site signals.
async function generateRecsWithClaude({ profile, signals, apiKey, model }) {
  const triggers  = ["wa-msg", "email-in", "monday", "forward", "review", "lead-new", "form-sub", "doc-up"];
  const actions   = ["answer", "reply", "book", "lead", "summary", "draft", "tag", "log"];
  const fallbacks = ["ping-phone", "ping-email", "ping-slack", "approval", "log-only"];
  const tools     = ["WhatsApp", "Email", "Sheets", "Stripe", "Calendar", "QuickBooks", "Phone", "Slack", "DocuSign", "Drive", "CRM", "Web"];

  const sigList = (signals || []).map((s) => `- ${s.label}${s.snippet ? ` ("${s.snippet.slice(0, 60)}")` : ""}`).join("\n");
  const userMsg = [
    `Recommend 4 AI agents for this specific business. Be specific, vivid, and tied to their actual operations — do NOT recommend generic agents.`,
    "",
    `Business profile:`,
    `- Company: ${profile.name || "(unnamed)"}`,
    `- Industry: ${profile.industry || "(unspecified)"}`,
    `- Time zone / location: ${profile.tz || "(unspecified)"}`,
    profile.website ? `- Website: ${profile.website}` : null,
    `- What they do: ${profile.desc || "(unspecified)"}`,
    sigList ? `- Detected on their website:\n${sigList}` : null,
    "",
    `Critical rules:`,
    `- These cards have to FIT in a small column. Stay tight. Hard limits:`,
    `    name:   ≤ 40 chars     (e.g. "Cairo Installation Scheduler")`,
    `    role:   ≤ 60 chars     ONE short phrase, lowercase, no period (e.g. "books on-site installations across Egypt")`,
    `    why:    2 sentences, ≤ 220 chars total. Specific to THIS business.`,
    `    impact: 1 sentence, ≤ 140 chars. Quantified outcome ("cuts quote turnaround from 2 days to 2 hours", "books 30% more tables when phones are busy"). NOT "saves time".`,
    `    tools:  2–4 tools max, drawn from: ${tools.join(", ")}`,
    `- Names are concrete and named for THIS business — not "Lead Grabber" or "Customer Helper". Good: "HVAC Quote Drafter", "Ramadan Hours Explainer", "Saudi Lead Reply".`,
    `- Reference real specifics from the description / signals — products, projects, places, the kind of customer.`,
    `- Every agent's flow uses ONLY these block IDs:`,
    `    triggers (when):   ${triggers.join(", ")}`,
    `    actions (do, 1-3): ${actions.join(", ")}`,
    `    fallbacks:         ${fallbacks.join(", ")}`,
    `- Pick a fitting emoji icon and a hex color (warm pastel, not pure black/white).`,
    `- If the business is non-English-speaking, the agent may need to speak the local language — say so in "why".`,
    "",
    `Return ONLY the JSON object that matches the schema.`,
  ].filter(Boolean).join("\n");

  const system = `You are a senior B2B operations consultant who recommends AI agents (digital employees) for small businesses. You're specific, opinionated, and concrete — never generic. You name agents after the exact job they'll do, you cite specifics from the business's website, and you quantify outcomes wherever possible. You only return JSON that matches the schema.`;

  const SERVER_URL = (window.CITRUS_CONFIG && window.CITRUS_CONFIG.SERVER_URL) || "http://localhost:3001";
  const res = await fetch(`${SERVER_URL}/api/chat`, {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify({
      model: model || "claude-haiku-4-5-20251001",
      max_tokens: 2500,
      system,
      messages: [{ role: "user", content: userMsg }],
      output_config: {
        format: {
          type: "json_schema",
          schema: {
            type: "object",
            properties: {
              recommendations: {
                type: "array",
                items: {
                  type: "object",
                  properties: {
                    name:    { type: "string" },
                    role:    { type: "string" },
                    icon:    { type: "string" },
                    color:   { type: "string" },
                    why:     { type: "string" },
                    impact:  { type: "string" },
                    tools:   { type: "array", items: { type: "string" } },
                    flow: {
                      type: "object",
                      properties: {
                        when:     { type: "string", enum: triggers },
                        do:       { type: "array", items: { type: "string", enum: actions } },
                        fallback: { type: "string", enum: fallbacks },
                      },
                      required: ["when", "do", "fallback"],
                      additionalProperties: false,
                    },
                  },
                  required: ["name", "role", "icon", "color", "why", "impact", "tools", "flow"],
                  additionalProperties: false,
                },
              },
            },
            required: ["recommendations"],
            additionalProperties: false,
          },
        },
      },
    }),
  });

  if (!res.ok) {
    const errText = await res.text().catch(() => "");
    throw new Error(`Server /api/chat ${res.status}: ${errText.slice(0, 200)}`);
  }
  const data = await res.json();
  const text = (data.content && data.content[0] && data.content[0].text) || "";
  const stripped = text.trim().replace(/^```(?:json)?\s*/i, "").replace(/```\s*$/i, "").trim();
  const start = stripped.indexOf("{");
  const end   = stripped.lastIndexOf("}");
  const json  = start >= 0 && end >= 0 ? stripped.slice(start, end + 1) : stripped;
  const parsed = JSON.parse(json);
  return Array.isArray(parsed.recommendations) ? parsed.recommendations : [];
}

// ============ RECOMMENDATIONS PANEL ============
function RecommendationsPanel({ profile, signals, companyName, onHire }) {
  // Heuristic fallback (always computed cheap, used when Claude isn't available
  // or hasn't been asked to think yet).
  const heuristicRecs = useMemo(
    () => recommendAgents(profile.industry, profile.desc, signals),
    [profile.industry, profile.desc, signals]
  );

  // Resolve API key (env or localStorage) and model
  const apiKey = (() => {
    const env = (typeof window !== "undefined" && window.CITRUS_CONFIG && window.CITRUS_CONFIG.CLAUDE_API_KEY) || "";
    if (env) return env;
    try { return localStorage.getItem("citrus_anthropic_key") || ""; } catch { return ""; }
  })();
  const model = (typeof window !== "undefined" && window.CITRUS_CONFIG && window.CITRUS_CONFIG.CLAUDE_MODEL) || "claude-haiku-4-5-20251001";

  // Claude recs persist across reloads — keyed by a hash of the profile so
  // we know when to invalidate.
  const inputHash = JSON.stringify({
    industry: profile.industry,
    desc: profile.desc,
    name: profile.name,
    tz: profile.tz,
    sigs: (signals || []).map((s) => s.id),
  });

  const [aiState, setAiState] = useState(() => {
    try {
      const saved = JSON.parse(localStorage.getItem("citrus_ai_recs") || "null");
      if (saved && saved.hash === inputHash) return { status: "ready", recs: saved.recs, hash: saved.hash, error: null };
    } catch {}
    return { status: "idle", recs: null, hash: null, error: null };
  });

  const generate = async () => {
    setAiState({ status: "loading", recs: null, hash: null, error: null });
    try {
      const recs = await generateRecsWithClaude({ profile, signals, apiKey, model });
      const out = { hash: inputHash, recs, ts: Date.now() };
      try { localStorage.setItem("citrus_ai_recs", JSON.stringify(out)); } catch {}
      setAiState({ status: "ready", recs, hash: inputHash, error: null });
    } catch (err) {
      setAiState({ status: "error", recs: null, hash: null, error: String(err.message || err).slice(0, 200) });
    }
  };

  // Auto-generate the first time after the profile has enough signal — once
  // industry + description are filled (or signals have been detected) and
  // we've never generated for this hash. Subsequent edits require pressing
  // the refresh button to avoid burning tokens on every keystroke.
  useEffect(() => {
    if (aiState.status !== "idle") return;
    const hasEnough = profile.industry && (profile.desc || (signals && signals.length > 0));
    if (!hasEnough) return;
    generate();
  }, [profile.industry, profile.desc, (signals || []).length]);

  const showAi = aiState.status === "ready" && aiState.hash === inputHash && Array.isArray(aiState.recs) && aiState.recs.length > 0;
  const recs = showAi ? aiState.recs : heuristicRecs;
  if (recs.length === 0) return null;

  const isStale = aiState.status === "ready" && aiState.hash !== inputHash;

  return (
    <div className="st-recs">
      <div className="st-recs-head">
        <div className="st-recs-head-l">
          <h3 className="st-recs-h">Recommended for {companyName || "your company"}</h3>
          <p className="st-recs-s">
            {showAi
              ? <>Tailored by Claude using {signals && signals.length ? `${signals.length} on-site signals + ` : ""}your industry, location, and what you do. Click any agent to drop into the architect with the rules pre-filled.</>
              : aiState.status === "loading"
                ? "Claude is reading your profile and writing tailored recommendations…"
                : apiKey
                  ? <>Showing baseline recommendations. <button type="button" className="st-website-link" onClick={generate}>Generate tailored picks with Claude</button></>
                  : <>Based on your industry and description. <strong>Connect a Claude API key</strong> in the Website field above for recommendations specific to your business.</>}
          </p>
        </div>
        {showAi || isStale ? (
          <button type="button" className="btn btn-ghost btn-sm st-recs-refresh" onClick={generate} disabled={aiState.status === "loading"}>
            {aiState.status === "loading" ? <><span className="st-website-spin" /> Thinking…</> : (isStale ? "Refresh — your profile changed" : "Refresh")}
          </button>
        ) : null}
      </div>

      {aiState.status === "error" ? (
        <div className="st-analysis-warn">Couldn't generate tailored picks: {aiState.error}. Showing baseline recommendations instead.</div>
      ) : null}

      <div className="st-recs-grid">
        {recs.map((r, i) => {
          const key = r.id || `ai-${i}`;
          // Defensive: even if Claude over-writes, never render absurdly long
          // role/why/impact text that breaks the card layout.
          const trim = (s, n) => {
            if (typeof s !== "string") return "";
            const t = s.replace(/\s+/g, " ").trim();
            return t.length > n ? t.slice(0, n - 1).trimEnd() + "…" : t;
          };
          const name   = trim(r.name, 50);
          const role   = trim(r.role, 80);
          const why    = trim(r.why, 260);
          const impact = trim(r.impact, 160);
          const tools  = (r.tools || []).slice(0, 4);
          return (
            <div key={key} className="st-rec st-rec-ai">
              <div className="st-rec-icon" style={{ background: r.color || "#E85D1A" }}>{r.icon || "✦"}</div>
              <div className="st-rec-body">
                <div className="st-rec-n">{name}</div>
                {role ? <div className="st-rec-r">{role}</div> : null}
                {why ? <div className="st-rec-w">{why}</div> : null}
                {impact ? (
                  <div className="st-rec-impact">
                    <span className="st-rec-impact-l">Impact</span>
                    <span>{impact}</span>
                  </div>
                ) : null}
                {tools.length > 0 ? (
                  <div className="st-rec-tools">
                    {tools.map((t) => <span key={t} className="st-rec-tool">{t}</span>)}
                  </div>
                ) : null}
                {!showAi && r.drivers && r.drivers.length > 0 ? (
                  <div className="st-rec-evidence">
                    Evidence: {r.drivers.map((d) => <span key={d.id} className="st-rec-evidence-chip">{d.label}</span>)}
                  </div>
                ) : null}
              </div>
              <button type="button" className="btn btn-primary btn-sm st-rec-hire" onClick={() => onHire(r)}>
                Hire
                <svg width="11" height="11" viewBox="0 0 12 12"><path d="M1 6h9M7 3l3 3-3 3" stroke="currentColor" strokeWidth="1.4" fill="none" strokeLinecap="round" strokeLinejoin="round"/></svg>
              </button>
            </div>
          );
        })}
      </div>
    </div>
  );
}

window.SettingsTab = SettingsTab;
// Expose the website analyser so onboarding.jsx (separate file) can reuse
// the exact same Claude prompt / HTML extraction / heuristic fallback path
// the Settings → Company "Analyze site" button uses. Returns the analysis
// object: { companyName, industry, tz, summary, location, products, ... }.
if (typeof window !== "undefined") window.citrusAnalyzeWebsite = analyzeWebsite;
