/* Conversational entry flow — ConversationalInput, UnderstoodPanel, TellUsMore,
   plus a deterministic mockExtract() that stands in for /api/extract-profile.

   Implements §2 of the addendum (v0.2). The real route calls
   buildExtractProfilePrompt() / parseExtractProfileResponse() server-side;
   here we mock the response shape so the UI can be designed end-to-end. */

const { useState: useStateCV, useEffect: useEffectCV, useRef: useRefCV } = React;

// Set this to your deployed Cloudflare Worker URL to enable the real LLM agent.
// Leave empty ("") to use the local regex mock (no API key needed).
const AGENT_API_URL = "https://fourjaw-roi.pages.dev";

/* ------------------------------------------------------------
   Example chips and placeholder copy
   ------------------------------------------------------------ */
const EXAMPLE_CHIPS = [
  "Subcon CNC, 10 machines, 2 shifts, looking at new capex",
  "Aerospace machining, 30 machines, three shifts",
  "Food & beverage line, 6 packaging machines, struggling with downtime",
];

const PLACEHOLDER_LONG =
  'e.g. "We run a 12-machine subcontract CNC shop in the Midlands, mainly aerospace work, ' +
  'two shifts five days. We\'re flat out and looking at a new VMC next year — probably around £180k."';

/* ------------------------------------------------------------
   FIELD_LABELS — for the Understood panel
   ------------------------------------------------------------ */
const FIELD_LABELS = {
  sector:                  "Sector",
  machineCount:            "Machines",
  shiftsPerDay:            "Shifts / day",
  daysPerWeek:             "Days / week",
  hoursPerShift:           "Hours / shift",
  chargeOutRate:           "Charge-out rate",
  currentUtilisationPct:   "Measured utilisation",
  manualDataHoursPerWeek:  "Manual data hrs / wk",
  demandConstrained:       "Demand-constrained",
  annualRevenue:           "Annual revenue",
  otdPenaltyExposure:      "OTD penalty exposure",
  plannedCapex:            "Planned capex",
  machineHoursPerWeek:     "Machine hours / week",
  trackingMaturity:        "Tracking maturity",
};

const FIELD_FORMAT = {
  sector:                  (v) => SECTOR_META[v]?.name || v,
  machineCount:            (v) => `${v} machine${v === 1 ? "" : "s"}`,
  shiftsPerDay:            (v) => `${v} shift${v === 1 ? "" : "s"} / day`,
  daysPerWeek:             (v) => `${v} days / week`,
  hoursPerShift:           (v) => `${v} hrs / shift`,
  chargeOutRate:           (v) => `£${v}/hr`,
  currentUtilisationPct:   (v) => `${v}%`,
  manualDataHoursPerWeek:  (v) => `${v} hrs / wk`,
  demandConstrained:       (v) => v ? "Yes — could sell extra hours" : "No — extra hours wouldn't sell",
  annualRevenue:           (v) => fmtGbp(v, { compact: true }),
  otdPenaltyExposure:      (v) => fmtGbp(v, { compact: true }) + " / yr",
  plannedCapex:            (v) => v?.price
                                    ? `${fmtGbp(v.price, { compact: true })} in ${v.deferralYears}y`
                                    : "—",
  machineHoursPerWeek:     (v) => `${v} hrs / week`,
  trackingMaturity:        (v) => {
    if (!v) return "—";
    const avg = (v.utilisation + v.downtime + v.jobs + v.energy + v.partCount) / 5;
    if (avg <= 0.5) return "Little to none";
    if (avg <= 1.5) return "Manual / spreadsheets";
    return "Some automated systems";
  },
};

/* ------------------------------------------------------------
   mockExtract — stand-in for POST /api/extract-profile
   Returns the same shape parseExtractProfileResponse() produces:
   { extracted, stated, inferred[], companyName, missingCritical, nextQuestion }
   ------------------------------------------------------------ */
// Applied after every mergeWithPrior that resolves a full extraction — injects
// the maturity follow-up if tracking data hasn't been gathered yet.
function withMaturityCheck(merged, priorExtraction) {
  if (
    !merged.extracted.trackingMaturity &&
    !priorExtraction?.extracted?.trackingMaturity &&
    merged.stated.length >= 1 &&
    (!merged.missingCritical || merged.missingCritical.length === 0)
  ) {
    // Leave trackingMaturity UNSET so nextBotQuestion detects it and asks naturally.
    merged.missingCritical = ["trackingMaturity"];
  }
  return merged;
}

function mockExtract(userText, priorExtraction) {
  const t = (userText || "").toLowerCase();

  // ----- canned scripted responses for design fidelity -----
  if (matchChip(t, "subcon cnc") && t.includes("10 machine")) {
    return withMaturityCheck(mergeWithPrior({
      extracted: {
        sector: "subcon_cnc", machineCount: 10, shiftsPerDay: 2,
        daysPerWeek: 5, plannedCapex: { price: 150000, deferralYears: 2 }, demandConstrained: true,
      },
      stated: ["sector", "machineCount", "shiftsPerDay", "plannedCapex"],
      inferred: [
        { field: "daysPerWeek", value: 5, reasoning: "'2 shifts' typically implies a Mon-Fri 2-shift pattern." },
        { field: "demandConstrained", value: true, reasoning: "Default for subcon CNC — demand assumed sellable." },
        { field: "plannedCapex.price", value: 150000, reasoning: "No price given; using the subcon CNC sector default for a new VMC." },
      ],
      companyName: null, missingCritical: [], nextQuestion: null,
    }, priorExtraction), priorExtraction);
  }

  if (matchChip(t, "aerospace") && t.includes("30 machine")) {
    return withMaturityCheck(mergeWithPrior({
      extracted: {
        sector: "aerospace", machineCount: 30, shiftsPerDay: 3,
        daysPerWeek: 7, demandConstrained: true,
      },
      stated: ["sector", "machineCount", "shiftsPerDay"],
      inferred: [
        { field: "daysPerWeek", value: 7, reasoning: "'Three shifts' on an aerospace floor usually means a 24/7 continuous pattern." },
        { field: "demandConstrained", value: true, reasoning: "Tier-1/2 aerospace usually runs against a fixed customer schedule." },
      ],
      companyName: null, missingCritical: [], nextQuestion: null,
    }, priorExtraction), priorExtraction);
  }

  if (matchChip(t, "food") && t.includes("packaging")) {
    return withMaturityCheck(mergeWithPrior({
      extracted: {
        sector: "food_beverage", machineCount: 6, shiftsPerDay: 3,
        daysPerWeek: 5, demandConstrained: true,
      },
      stated: ["sector", "machineCount"],
      inferred: [
        { field: "shiftsPerDay", value: 3, reasoning: "FMCG packaging lines typically run 3-shift to keep changeover-amortised throughput." },
        { field: "daysPerWeek", value: 5, reasoning: "Standard 5-day FMCG pattern." },
        { field: "demandConstrained", value: true, reasoning: "User mentioned 'struggling with downtime' — implies orders aren't the bottleneck." },
      ],
      companyName: null, missingCritical: [], nextQuestion: null,
    }, priorExtraction), priorExtraction);
  }

  // ----- the worked example (§8 of the addendum) -----
  if (t.includes("20-machine") || (t.includes("coventry") && t.includes("motorsport"))) {
    return withMaturityCheck(mergeWithPrior({
      extracted: {
        sector: "subcon_cnc", machineCount: 20, shiftsPerDay: 2,
        daysPerWeek: 5, chargeOutRate: 60,
        plannedCapex: { price: 250000, deferralYears: 2 }, demandConstrained: true,
      },
      stated: ["sector", "machineCount", "shiftsPerDay", "daysPerWeek", "chargeOutRate", "plannedCapex"],
      inferred: [
        { field: "demandConstrained", value: true, reasoning: "User said 'absolutely flat out' and 'lead times have crept up'." },
        { field: "sector", value: "subcon_cnc", reasoning: "User said 'subcontract CNC shop' — aerospace and motorsport are customer industries, not their sector." },
      ],
      companyName: null, missingCritical: [], nextQuestion: null,
    }, priorExtraction), priorExtraction);
  }

  // ----- "Tell us more" — short fragments that extend prior -----
  if (priorExtraction && t.match(/charge[- ]?out|£\s*\d+\/?\s*hr|hour|\d+\s*\/?\s*hr/)) {
    const rateMatch = t.match(/£?\s*(\d{2,3})\s*\/?\s*(?:hr|hour|per hour)/);
    const rate = rateMatch ? Number(rateMatch[1]) : null;
    const capexMatch = t.match(/£?\s*(\d{2,3})\s*k|£?\s*(\d{3,7})/);
    const capex = capexMatch ? (capexMatch[1] ? Number(capexMatch[1]) * 1000 : Number(capexMatch[2])) : null;
    const newFields = rate ? { chargeOutRate: rate } : {};
    const newStated = rate ? ["chargeOutRate"] : [];
    const newInferred = [];
    if (capex && capex > 30000) { newFields.plannedCapex = { price: capex, deferralYears: 2 }; newStated.push("plannedCapex"); }
    return withMaturityCheck(mergeWithPrior({
      extracted: newFields, stated: newStated, inferred: newInferred,
      companyName: null, missingCritical: [], nextQuestion: null,
    }, priorExtraction), priorExtraction);
  }

  if (priorExtraction && (t.match(/walk(ing)? round/) || t.includes("writing down job"))) {
    return withMaturityCheck(mergeWithPrior({
      extracted: { manualDataHoursPerWeek: 6 },
      stated: ["manualDataHoursPerWeek"], inferred: [],
      companyName: null, missingCritical: [], nextQuestion: null,
    }, priorExtraction), priorExtraction);
  }

  // ----- best-effort regex fallback for anything else -----
  const fallback = regexExtract(userText || "");

  // ----- nothing useful → ask the one question that unblocks the most -----
  if (!fallback.extracted.sector && fallback.stated.length < 2 && !priorExtraction?.extracted?.sector) {
    return {
      extracted: priorExtraction?.extracted || {},
      stated: priorExtraction?.stated || [],
      inferred: priorExtraction?.inferred || [],
      companyName: null,
      missingCritical: ["sector"],
      nextQuestion: {
        field: "sector",
        prompt: "Got it. What kind of operation is this?",
        options: [
          { value: "subcon_cnc",    label: "Subcon CNC" },
          { value: "aerospace",     label: "Aerospace" },
          { value: "automotive",    label: "Automotive" },
          { value: "food_beverage", label: "Food & beverage" },
          { value: "metals",        label: "Metals" },
          { value: "medical",       label: "Medical" },
          { value: "plastics",      label: "Plastics" },
        ],
      },
    };
  }

  return withMaturityCheck(mergeWithPrior(fallback, priorExtraction), priorExtraction);
}

function matchChip(t, key) { return t.indexOf(key) !== -1; }

// Score a free-text answer about one tracking dimension: 0 = nothing, 1 = manual, 2 = system
function scoreMaturityAnswer(text) {
  const t = (text || "").toLowerCase().trim();
  if (!t || t.length < 3) return 1;
  if (t.match(/nothing|don'?t.{0,12}track|no system|not tracked|not really|no way|don'?t know|n\/a|^na$|^none$|^skip$|no idea|not measured|we don'?t|no visibility/)) return 0;
  if (t.match(/system|software|erp|mes|scada|plc|sensor|monitor|platform|digital|dashboard|app|automat|live data|real.?time|oee|integrated|measured|counted|machine count/)) return 2;
  // default: something typed = at least manual
  return t.length > 5 ? 1 : 0;
}

// Decide what the bot should ask next — SDR-style SPICED discovery.
// Returns { field, text } or null to push to results.
// turnCount = number of user messages sent so far.
function nextBotQuestion(extraction, turnCount) {
  const e = extraction?.extracted || {};
  const tc = turnCount || 0;
  const sectorLabel = e.sector ? (SECTOR_META[e.sector]?.name || e.sector) : null;
  const hasHours = e.machineHoursPerWeek || e.shiftsPerDay;
  const hasRate  = e.chargeOutRate || e.chargeOutSkipped;
  const m = e.machineCount;

  // Hard cap — 15 messages max, or after 5 turns with minimum data
  if (tc >= 15) return null;
  if (tc >= 5 && e.sector && m != null) return null;

  // Need sector first
  if (!e.sector) return {
    field: "sector",
    text: tc === 0
      ? "Tell me about your operation — what sector, roughly how many machines, and what's prompted you to look at this now?"
      : "What kind of manufacturing is this — what sector and what do you produce?"
  };

  // Have sector, need machine count
  if (m == null) return {
    field: "machineCount",
    text: `${sectorLabel} — good. How many machines are we talking?`
  };

  // Turn 1: situation + pain + first data ask (all in one natural question)
  if (tc <= 1 && !hasHours && !hasRate) return {
    field: "machineHoursPerWeek",
    text: `${m} ${sectorLabel} machine${m === 1 ? '' : 's'} — that's a decent floor. What's the main thing you're trying to fix or get on top of — utilisation, unplanned downtime, visibility, something else? And how many hours a week are those machines running, and what do you charge out per hour?`
  };

  // Have some pain context but still missing hours or rate — targeted ask
  if (!hasHours) return {
    field: "machineHoursPerWeek",
    text: "How many hours a week are those machines running?"
  };
  if (!hasRate) return {
    field: "chargeOutRate",
    text: "And your charge-out rate per machine-hour — rough ballpark is fine. Say 'not sure' to skip."
  };

  // Have hours + rate — ask about business context / critical event / capex (turn 3-4)
  if (tc <= 3 && !e.criticalEventCaptured) return {
    field: "criticalEvent",
    text: `What's making this timely — is there a contract review, a capex decision, customer pressure, or something on the horizon that's sharpened the focus? And is there any planned equipment investment I should factor in?`
  };

  // Enough — push to results
  return null;
}

// Returns true if the user's message is clearly off-topic (not about their manufacturing operation)
function isOffTopic(text) {
  const t = text.toLowerCase();
  if (/what (is|are) fourjaw|how does fourjaw|your pricing|how much does (fourjaw|this|it) cost|tell me about yourself|what can you do|who are you|are you (an? )?ai|chat.?gpt|gpt|claude|openai/.test(t)) return true;
  // Anything with a clear manufacturing keyword is on-topic
  if (/machine|manufactur|cnc|shift|capacity|downtime|utilisation|hours|charge|sector|aerospace|automotive|food|medical|plastics|metals|operator|plant|floor|job|batch|output|revenue|capex|contract|tool|tooling|press|vmc|lathe|mill|rivet|weld|assembly/.test(t)) return false;
  // Very short answers are probably on-topic (just answering the question)
  if (text.trim().split(/\s+/).length <= 6) return false;
  return false;
}

// Build a human-readable summary of what was found vs. what will be defaulted.
function summariseExtraction(extraction) {
  const e = extraction?.extracted || {};
  const found = [];
  const defaulted = [];
  const sectorName = e.sector ? (SECTOR_META[e.sector]?.name || e.sector) : null;
  if (sectorName)              found.push(sectorName);
  if (e.machineCount)          found.push(`${e.machineCount} machine${e.machineCount === 1 ? '' : 's'}`);
  if (e.machineHoursPerWeek)   found.push(`${e.machineHoursPerWeek} hrs/week`);
  else if (e.shiftsPerDay)     found.push(`${e.shiftsPerDay}-shift`);
  else                         defaulted.push("machine hours");
  if (e.chargeOutRate)         found.push(`£${e.chargeOutRate}/hr charge-out`);
  else if (!e.chargeOutSkipped) defaulted.push("charge-out rate");
  let msg = "Good — I've got enough to run the numbers.";
  if (found.length)     msg += ` Picked up: ${found.join(", ")}.`;
  if (defaulted.length) msg += ` I'll use ${sectorName || 'sector'} defaults for ${defaulted.join(" and ")}.`;
  msg += " You can fine-tune everything on the results page. Anything else before I show you?";
  return msg;
}

// Parse a charge-out rate from natural language: "like £100", "£65/hr", "about 80 quid"
function parseChargeOutRate(text) {
  const t = text.toLowerCase();
  const withHr  = t.match(/(\d{2,4})\s*(?:\/hr|per hour|\/hour|per hr|\s*ph\b)/);
  const withGbp = t.match(/£\s*(\d{2,4})/);
  const quid    = t.match(/(\d{2,4})\s*(?:quid|pounds?)/);
  const n = withHr ? Number(withHr[1]) : withGbp ? Number(withGbp[1]) : quid ? Number(quid[1]) : null;
  return (n && n >= 20 && n <= 2000) ? n : null;
}

// Parse machine hours/week from natural language: "7 days × 2 × 8h", "80 hrs/week", "100 hour weeks", "3 shifts 5 days"
function parseMachineHours(text) {
  const t = text.toLowerCase()
    .replace(/[×*]/g, 'x')
    .replace(/\bseven\b/g, '7').replace(/\bsix\b/g, '6').replace(/\bfive\b/g, '5')
    .replace(/\bfour\b/g, '4').replace(/\bthree\b/g, '3').replace(/\btwo\b/g, '2').replace(/\bone\b/g, '1');
  // Direct: "80 hours a week", "100 hour weeks", "80hrs/week"
  const direct = t.match(/(\d{2,3})\s*h(?:r|our)?s?\s*(?:\/|per\s+|a\s+|-\s*)?\s*weeks?/);
  if (direct) return Math.min(168, Number(direct[1]));
  const dM = t.match(/(\d+)\s*days?/);
  const sM = t.match(/(\d)\s*(?:x|shifts?)\s/);
  const hS = t.match(/(\d+)\s*h(?:r|our)?s?\s*(?:a\s+|per\s+)?shift/);
  const hA = t.match(/\b(\d+)\s*h(?:r|our)?s?\b/);
  const days   = dM ? Math.min(7,  Number(dM[1])) : null;
  const shifts = sM ? Math.min(3,  Number(sM[1])) : null;
  const hps    = hS ? Math.min(16, Number(hS[1])) : (hA ? Math.min(16, Number(hA[1])) : null);
  if (days && shifts && hps) return days * shifts * hps;
  if (days && shifts)        return days * shifts * 8;
  if (shifts && hps)         return shifts * hps * 5;
  if (days && hps)           return days * hps;
  // Loose fallback: "100 hours" standalone with no week context — plausible weekly-hours range
  if (hA && !days && !shifts) {
    const n = Number(hA[1]);
    if (n >= 20 && n <= 168) return n;
  }
  return null;
}

// Score tracking maturity from a single free-text chat reply.
function scoreTrackingFromChat(text) {
  const t = text.toLowerCase();
  const hasSystem  = /erp|mes|scada|plc|sensor|automat|digital|dashboard|software|live data|real.?time/.test(t);
  const hasManual  = /spreadsheet|whiteboard|paper|log|manually|written|clipboard|walk|by eye|estimate|guess/.test(t);
  const hasNothing = /nothing|don'?t|no idea|not track|no system|not measured|no way|we don'?t|no visibility/.test(t);
  const global = hasNothing ? 0 : hasSystem ? 2 : hasManual ? 1 : 1;
  const dim = (keywords) => {
    const mentioned = keywords.some(k => t.includes(k));
    if (!mentioned) return global;
    if (hasSystem && mentioned) return 2;
    if (hasNothing && mentioned) return 0;
    return global;
  };
  return {
    utilisation: dim(["runtime", "utilisation", "utilization", "uptime", "oee", "running time"]),
    downtime:    dim(["downtime", "stoppage", "breakdown", "maintenance", "fault"]),
    jobs:        dim(["job", "batch", "work order", "traveller", "schedule", "order"]),
    energy:      dim(["energy", "power", "electricity", "kwh"]),
    partCount:   dim(["part", "output", "piece", "count", "unit", "cycle"]),
  };
}

/* ------------------------------------------------------------
   Maturity groups — thresholds based on average dim score (0–2)
   ------------------------------------------------------------ */
const MATURITY_GROUPS = [
  {
    name:    "Foundational",
    label:   "Little to no current visibility",
    maxAvg:  0.4,
    pitch:   "With limited operational visibility right now, there's a substantial opportunity to unlock improvements — the ROI headroom here is typically at the high end.",
    colour:  "#e05c5c",
  },
  {
    name:    "Reactive",
    label:   "Informal tracking, reacting to issues",
    maxAvg:  1.0,
    pitch:   "You're picking some things up by eye and informally, but the gaps mean issues often go unnoticed until they've already cost you. Closing those gaps tends to deliver measurable returns quickly.",
    colour:  "#d99a3a",
  },
  {
    name:    "Aspirational",
    label:   "Manual processes, clear gaps remain",
    maxAvg:  1.6,
    pitch:   "You have manual processes in place and a clear sense of where the gaps are — which puts you in a strong position to make the jump to real-time visibility. The improvement headroom is significant.",
    colour:  "var(--fj-purple)",
  },
  {
    name:    "Established",
    label:   "Good foundation, some gaps to close",
    maxAvg:  Infinity,
    pitch:   "You already have solid operational visibility. The focus here is on closing the remaining gaps and optimising what you have.",
    colour:  "var(--fj-success)",
  },
];

// Score one tracking dimension by looking for keywords in their surrounding context.
// Returns 0 (gap), 1 (manual), or 2 (automated); or a baseline when unmentioned.
function _scoreDim(text, hasLiveSystem, hasManual, hasGlobalNothing, keywords) {
  const idx = keywords.map(k => text.indexOf(k)).filter(i => i >= 0);
  if (!idx.length) {
    // Unmentioned — fall back to global signals
    if (hasGlobalNothing) return 0;
    return hasLiveSystem ? 2 : hasManual ? 1 : 1;
  }
  const first = Math.min(...idx);
  const ctx = text.slice(Math.max(0, first - 70), first + 70);
  const isGap = /don'?t|no (way|idea|system|visibility|tracking)|not (tracked|measured|sure)|nothing|can'?t|until someone|never|blind/.test(ctx);
  if (isGap) return 0;
  if (hasLiveSystem) return 2;
  if (hasManual) return 1;
  return 0.5;
}

// Infer maturity dims + group from the full chat transcript.
function inferMaturityFromChat(transcriptTexts, extraction) {
  const allText = (transcriptTexts || []).join(" ").toLowerCase();
  const e = extraction?.extracted || {};

  const hasLiveSystem  = /live data|real.?time|automated|scada|mes|sensor|oee platform/.test(allText);
  const hasAnySoftware = /erp|software|dashboard|digital/.test(allText);
  const hasManual      = /spreadsheet|manually|paper|log|written|clipboard|whiteboard|estimate/.test(allText);
  // Global "we track nothing" signal — only when explicitly stated
  const hasGlobalNothing = /don'?t.{0,20}track.{0,20}(anything|nothing|at all)|track nothing|no tracking at all|nothing.{0,20}tracked|completely.{0,15}blind/.test(allText);

  const hasSystem = hasLiveSystem || hasAnySoftware;

  // Always re-score from transcript — regexExtract's maturity is too noisy to trust.
  // Only fall back to explicit extraction if it was set from a dedicated maturity question.
  const dims = {
    utilisation: _scoreDim(allText, hasLiveSystem, hasManual, hasGlobalNothing,
      ["utilisation", "utilization", "runtime", "running time", "oee", "uptime", "machine hours"]),
    downtime:    _scoreDim(allText, hasLiveSystem, hasManual, hasGlobalNothing,
      ["downtime", "breakdown", "stoppage", "unreliable", "go down", "goes down", "machine down"]),
    jobs:        _scoreDim(allText, hasLiveSystem, hasManual, hasGlobalNothing,
      ["work order", "job track", "job schedul", "traveller", "order track", "batch track"]),
    energy:      _scoreDim(allText, hasLiveSystem, hasManual, hasGlobalNothing,
      ["energy", "electricity", "kwh", "power consumption"]),
    partCount:   _scoreDim(allText, hasLiveSystem, hasManual, hasGlobalNothing,
      ["part count", "output", "throughput", "pieces per", "cycle time"]),
  };

  // Build evidence — what they actually mentioned as gaps
  const evidence = [];
  if (dims.utilisation === 0) evidence.push("machine utilisation and running time");
  if (dims.downtime    === 0) evidence.push("downtime and stoppages");
  if (dims.jobs        === 0) evidence.push("job and order tracking");
  if (hasManual && !hasSystem) evidence.push("manual data collection");
  if (!evidence.length && !hasSystem) evidence.push("your current tracking setup");

  const avg = (dims.utilisation + dims.downtime + dims.jobs + dims.energy + dims.partCount) / 5;
  const group = MATURITY_GROUPS.find(g => avg < g.maxAvg) || MATURITY_GROUPS[MATURITY_GROUPS.length - 1];

  return {
    dims,
    groupName:  group.name,
    groupLabel: group.label,
    pitch:      group.pitch,
    colour:     group.colour,
    evidence,
    avg,
  };
}

// Analyse conversation transcript for qualitative opportunity signals.
function analyseOpportunity(transcriptTexts) {
  const t = (transcriptTexts || []).join(" ").toLowerCase();
  const signals = [];
  let score = 0;
  if (/flat out|at capacity|maxed|no spare|stretched|run.{0,10}flat/.test(t))                          { signals.push("Running at or near full capacity"); score += 2; }
  if (/lead time|delivery|late|miss.{0,10}deadline|behind schedule/.test(t))                            { signals.push("Delivery or lead-time pressure"); score += 2; }
  if (/downtime|breakdown|stoppage|unreliable|machine.{0,10}(down|stop|break)/.test(t))                 { signals.push("Unplanned downtime issues"); score += 1; }
  if (/new machine|capex|invest|five.?axis|vmc|new kit|new equipment/.test(t) && /thinking|plan|consider|looking/.test(t)) { signals.push("Evaluating capital investment"); score += 2; }
  if (/don'?t know|nothing|whiteboard|spreadsheet|manually|no (visibility|idea|system)|by eye/.test(t)) { signals.push("Low current operational visibility"); score += 2; }
  if (/penalty|fine|contract.{0,10}risk|lose.{0,10}contract|losing work/.test(t))                      { signals.push("Contract or penalty risk"); score += 2; }
  if (/growing|more work|more orders|can'?t keep up|turning.{0,10}work away/.test(t))                   { signals.push("Active demand growth"); score += 1; }
  const level = score >= 5 ? "High" : score >= 2 ? "Medium" : "Low";
  const levelColor = score >= 5 ? "var(--fj-success)" : score >= 2 ? "#d99a3a" : "var(--fg-3)";
  return { signals, score, level, levelColor };
}

function regexExtract(userText) {
  const t = userText.toLowerCase();
  const extracted = {};
  const stated = [];
  const inferred = [];

  // sector
  const sectorMap = [
    ["subcon",      "subcon_cnc"],
    ["cnc shop",    "subcon_cnc"],
    ["machine shop","subcon_cnc"],
    ["aerospace",   "aerospace"],
    ["automotive",  "automotive"],
    ["food",        "food_beverage"],
    ["beverage",    "food_beverage"],
    ["packaging",   "food_beverage"],
    ["metals",      "metals"],
    ["forging",     "metals"],
    ["casting",     "metals"],
    ["medical",     "medical"],
    ["plastics",    "plastics"],
    ["moulding",    "plastics"],
    ["injection",   "plastics"],
  ];
  for (const [k, v] of sectorMap) {
    if (t.includes(k)) { extracted.sector = v; stated.push("sector"); break; }
  }

  // machines
  const m = t.match(/(\d{1,3})\s*(?:machine|machines|cnc|vmc|mill|lathe|line|press)/);
  if (m) { extracted.machineCount = Number(m[1]); stated.push("machineCount"); }

  // shifts
  if (t.includes("three shift") || t.includes("3 shift") || t.includes("3-shift") || t.includes("24/7") || t.includes("continuous")) {
    extracted.shiftsPerDay = 3;
    extracted.daysPerWeek = 7;
    stated.push("shiftsPerDay");
    inferred.push({ field: "daysPerWeek", value: 7, reasoning: "3-shift continuous pattern implies 7-day operation." });
  } else if (t.includes("two shift") || t.includes("2 shift") || t.includes("2-shift") || t.includes("back shift")) {
    extracted.shiftsPerDay = 2;
    extracted.daysPerWeek = 5;
    stated.push("shiftsPerDay");
    inferred.push({ field: "daysPerWeek", value: 5, reasoning: "2-shift typically implies Mon-Fri pattern." });
  } else if (t.includes("one shift") || t.includes("1 shift") || t.includes("day shift")) {
    extracted.shiftsPerDay = 1;
    extracted.daysPerWeek = 5;
    stated.push("shiftsPerDay");
  }

  // charge-out rate
  const r = t.match(/£?\s*(\d{2,3})\s*(?:\/|per)?\s*(?:hr|hour|per hour)/);
  if (r) { extracted.chargeOutRate = Number(r[1]); stated.push("chargeOutRate"); }

  // capex
  const cx = t.match(/£\s*(\d{2,4})\s*k|£\s*(\d{4,7})/);
  if (cx && (t.includes("capex") || t.includes("buy") || t.includes("new machine") || t.includes("vmc") || t.includes("5-axis"))) {
    const price = cx[1] ? Number(cx[1]) * 1000 : Number(cx[2]);
    if (price >= 30000) {
      extracted.plannedCapex = { price, deferralYears: 2 };
      stated.push("plannedCapex");
    }
  }

  // demand
  if (t.includes("flat out") || t.includes("flat-out") || t.includes("lead time")) {
    extracted.demandConstrained = true;
    inferred.push({ field: "demandConstrained", value: true, reasoning: "Phrases like 'flat out' / 'lead times' imply orders aren't the bottleneck." });
  }

  // tracking maturity
  if (t.includes("nothing") || t.includes("don't track") || t.includes("no system") || t.includes("no tracking")) {
    const maturity = { utilisation: 0, downtime: 0, jobs: 0, energy: 0, partCount: 0 };
    extracted.trackingMaturity = maturity;
    stated.push("trackingMaturity");
  } else if (t.includes("manually") || t.includes("whiteboard") || t.includes("spreadsheet") || t.includes("paper")) {
    const maturity = { utilisation: 1, downtime: 1, jobs: 1, energy: 1, partCount: 1 };
    extracted.trackingMaturity = maturity;
    stated.push("trackingMaturity");
  } else if ((t.includes("system") && !/no.{0,8}system|without.{0,8}system/.test(t)) || t.includes("software") || t.includes("erp") || t.includes("mes") || t.includes("automated")) {
    const maturity = { utilisation: 2, downtime: 2, jobs: 2, energy: 2, partCount: 2 };
    extracted.trackingMaturity = maturity;
    stated.push("trackingMaturity");
  }

  return { extracted, stated, inferred, companyName: null, missingCritical: [], nextQuestion: null };
}

function mergeWithPrior(result, prior) {
  if (!prior) return result;
  const extracted = { ...prior.extracted, ...result.extracted };
  const stated = Array.from(new Set([...(prior.stated || []), ...(result.stated || [])]));
  const inferred = [...(prior.inferred || []), ...(result.inferred || [])];
  return {
    ...result,
    extracted,
    stated,
    inferred,
    companyName: result.companyName || prior.companyName || null,
  };
}

/* ChatConversation — proper back-and-forth chat that collects all required fields.
   Calls onResult(extraction, transcript) when the user is ready to see numbers. */
function ChatConversation({ onResult, dark }) {
  const [messages,   setMessages]   = useStateCV([]);
  const [input,      setInput]      = useStateCV("");
  const [extraction, setExtraction] = useStateCV(null);
  const [isThinking, setIsThinking] = useStateCV(false);
  const [chatStep,   setChatStep]   = useStateCV("collecting"); // collecting | wrapping_up
  const [transcript, setTranscript] = useStateCV([]);
  const [turnCount,    setTurnCount]   = useStateCV(0);
  const [apiMessages,  setApiMessages] = useStateCV([]); // clean history for the real agent
  const scrollRef    = useRefCV(null);
  const inputRef     = useRefCV(null);
  const chatStepRef  = useRefCV("collecting");
  const turnCountRef = useRefCV(0);

  function scrollToBottom() {
    requestAnimationFrame(() => {
      if (scrollRef.current) scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
    });
  }

  function cleanBotText(text) {
    return text
      .replace(/\*\*(.+?)\*\*/g, '$1')       // **bold** → text
      .replace(/\*(.+?)\*/g, '$1')            // *italic* → text
      .replace(/^#{1,4}\s+/gm, '')            // ## headers
      .replace(/^\s*[-•]\s+/gm, '')           // bullet points
      .replace(/\|.*\|/g, '')                 // table rows
      .replace(/^[-|:\s]+$/gm, '')            // table separators
      .replace(/\n{3,}/g, '\n\n')             // collapse excessive blank lines
      .trim();
  }

  function addBotMessage(text) {
    const clean = cleanBotText(text);
    const id = Date.now() + Math.random();
    setMessages(m => [...m, { role: "bot", typing: true, id }]);
    const delay = Math.min(400 + clean.length * 6, 1400);
    setTimeout(() => {
      setMessages(m => m.map(msg => msg.id === id ? { ...msg, typing: false, text: clean } : msg));
      scrollToBottom();
    }, delay);
  }

  useEffectCV(() => {
    addBotMessage("What sector are you in, how many machines, and what's the main problem you're trying to solve?");
  }, []);

  useEffectCV(() => { scrollToBottom(); }, [messages.length]);

  // --- Real agent path: call the Cloudflare Worker ---
  async function callAgent(userText, currentExtraction, currentApiMessages) {
    const newApiMessages = [...currentApiMessages, { role: "user", content: userText }];
    setApiMessages(newApiMessages);

    const res = await fetch(`${AGENT_API_URL}/chat`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ messages: newApiMessages }),
    });
    if (!res.ok) throw new Error(`Agent returned ${res.status}`);
    const data = await res.json();

    // Merge whatever the agent extracted into our running extraction state
    const apiFields = data.extraction || {};
    const newExtraction = Object.keys(apiFields).length
      ? {
          extracted: { ...(currentExtraction?.extracted || {}), ...apiFields },
          stated: Array.from(new Set([
            ...(currentExtraction?.stated || []),
            ...Object.keys(apiFields),
          ])),
          inferred: currentExtraction?.inferred || [],
          companyName: currentExtraction?.companyName || null,
          missingCritical: [],
          nextQuestion: null,
        }
      : currentExtraction;

    setExtraction(newExtraction);
    // Append the assistant's reply to the API history for next turn
    setApiMessages([...newApiMessages, { role: "assistant", content: data.reply }]);

    return { reply: data.reply, extraction: newExtraction, readyForROI: data.readyForROI };
  }

  // --- Mock fallback path: local regex pipeline ---
  function callMock(userText, currentExtraction) {
    const next = nextBotQuestion(currentExtraction, turnCountRef.current - 1);
    let newExtraction = mockExtract(userText, currentExtraction);

    let hrs = parseMachineHours(userText);
    if (!hrs) {
      const pctM = userText.match(/(\d+)\s*%\s*(?:of\s*(?:that|it))?/i);
      const halfM = /\bhalf\b/.test(userText.toLowerCase());
      const pct = pctM ? Number(pctM[1]) / 100 : halfM ? 0.5 : null;
      if (pct) {
        for (let i = transcript.length - 1; i >= 0; i--) {
          const prev = parseMachineHours(transcript[i]);
          if (prev) { hrs = Math.round(prev * pct); break; }
          const raw = transcript[i].match(/\b(\d{2,3})\s*hour/i);
          if (raw) { hrs = Math.round(Number(raw[1]) * pct); break; }
        }
      }
    }
    if (hrs && !newExtraction.extracted.machineHoursPerWeek) {
      newExtraction = { ...newExtraction, extracted: { ...newExtraction.extracted, machineHoursPerWeek: hrs },
        stated: Array.from(new Set([...(newExtraction.stated || []), "machineHoursPerWeek"])) };
    }
    const rate = parseChargeOutRate(userText);
    if (rate) {
      newExtraction = { ...newExtraction, extracted: { ...newExtraction.extracted, chargeOutRate: rate },
        stated: Array.from(new Set([...(newExtraction.stated || []), "chargeOutRate"])) };
    } else if (!newExtraction.extracted.chargeOutRate && !newExtraction.extracted.chargeOutSkipped &&
               /not sure|don'?t know|skip|no idea|unsure|pass/.test(userText.toLowerCase())) {
      newExtraction = mergeWithPrior(
        { extracted: { chargeOutSkipped: true }, stated: [], inferred: [], companyName: null, missingCritical: [], nextQuestion: null },
        newExtraction
      );
    }
    const capexM = userText.match(/£\s*(\d{1,4})\s*k\b|£\s*(\d{4,7})\b/i);
    if (capexM && !newExtraction.extracted.plannedCapex) {
      const price = capexM[1] ? Number(capexM[1]) * 1000 : Number(capexM[2]);
      if (price >= 30000) {
        newExtraction = { ...newExtraction, extracted: { ...newExtraction.extracted, plannedCapex: { price, deferralYears: 2 } },
          stated: Array.from(new Set([...(newExtraction.stated || []), "plannedCapex"])) };
      }
    }
    if (next?.field === "criticalEvent") {
      newExtraction = { ...newExtraction, extracted: { ...newExtraction.extracted, criticalEventCaptured: true } };
    }
    setExtraction(newExtraction);

    const tc = turnCountRef.current;
    const nextQ = nextBotQuestion(newExtraction, tc);
    const readyForROI = nextQ === null;
    const reply = readyForROI ? summariseExtraction(newExtraction) : nextQ.text;
    return { reply, extraction: newExtraction, readyForROI };
  }

  // --- Unified entry point ---
  async function processReply(userText, currentExtraction) {
    setIsThinking(true);
    const tc = turnCountRef.current;
    turnCountRef.current = tc + 1;
    setTurnCount(tc + 1);

    // wrapping_up: any extra context → push straight to results
    if (chatStepRef.current === "wrapping_up") {
      const finalExtraction = AGENT_API_URL
        ? currentExtraction
        : mockExtract(userText, currentExtraction);
      const finalTranscript = [...transcript, userText];
      setIsThinking(false);
      const suggest = inferMaturityFromChat(finalTranscript, finalExtraction);
      onResult({
        ...finalExtraction,
        extracted: { ...finalExtraction?.extracted, trackingMaturity: finalExtraction?.extracted?.trackingMaturity || suggest.dims },
        maturitySuggestion: suggest,
      }, finalTranscript);
      return;
    }

    try {
      let result;
      if (AGENT_API_URL) {
        // Always run local parsers first so extraction builds up even when the
        // agent doesn't return tool calls (e.g. OpenRouter fallback path)
        let localExt = mockExtract(userText, currentExtraction);
        const hrs = parseMachineHours(userText);
        if (hrs && !localExt.extracted.machineHoursPerWeek)
          localExt = { ...localExt, extracted: { ...localExt.extracted, machineHoursPerWeek: hrs },
            stated: Array.from(new Set([...(localExt.stated || []), "machineHoursPerWeek"])) };
        const rate = parseChargeOutRate(userText);
        if (rate)
          localExt = { ...localExt, extracted: { ...localExt.extracted, chargeOutRate: rate },
            stated: Array.from(new Set([...(localExt.stated || []), "chargeOutRate"])) };
        const capexM = userText.match(/£\s*(\d{1,4})\s*k\b|£\s*(\d{4,7})\b/i);
        if (capexM && !localExt.extracted.plannedCapex) {
          const price = capexM[1] ? Number(capexM[1]) * 1000 : Number(capexM[2]);
          if (price >= 30000)
            localExt = { ...localExt, extracted: { ...localExt.extracted, plannedCapex: { price, deferralYears: 2 } },
              stated: Array.from(new Set([...(localExt.stated || []), "plannedCapex"])) };
        }
        result = await callAgent(userText, localExt, apiMessages);

        // Local readyForROI fallback: fires when we have enough SPICED data.
        // Requires 4+ turns so the agent has time to probe pain + tracking gaps.
        const e = result.extraction?.extracted || {};
        const hasCapacity = !!(e.machineHoursPerWeek || e.shiftsPerDay || e.chargeOutRate);
        const hasPain = !!(e.trackingMaturity || e.criticalEvent || e.demandConstrained != null);
        const localReady = turnCountRef.current >= 4 &&
          !!(e.sector && e.machineCount != null && hasCapacity && hasPain);
        if (localReady && !result.readyForROI) result = { ...result, readyForROI: true };
      } else {
        result = callMock(userText, currentExtraction);
      }

      setIsThinking(false);

      if (result.readyForROI) {
        chatStepRef.current = "wrapping_up";
        setChatStep("wrapping_up");
      }
      addBotMessage(result.reply);
    } catch (err) {
      console.error("Agent error:", err);
      setIsThinking(false);
      addBotMessage("Connection dropped — could you say that again?");
    }
  }

  function handleSend() {
    const text = input.trim();
    if (!text || isThinking) return;
    setInput("");
    const newTranscript = [...transcript, text];
    setTranscript(newTranscript);
    setMessages(m => [...m, { role: "user", text, id: Date.now() }]);
    scrollToBottom();
    processReply(text, extraction);
  }

  const canProceed = chatStep === "wrapping_up";
  const e0 = extraction?.extracted || {};
  const canEarlySubmit = chatStep === "collecting" && turnCount >= 5 &&
    e0.sector && e0.machineCount != null &&
    (e0.machineHoursPerWeek || e0.shiftsPerDay || e0.chargeOutRate);

  return (
    <div className={"chat-shell" + (dark ? " chat-shell--dark" : "")}>
      <div className="chat-messages" ref={scrollRef}>
        {messages.map((msg) => (
          <div key={msg.id} className={"chat-bubble chat-bubble--" + msg.role}>
            {msg.role === "bot" && <div className="chat-bubble__avatar">FJ</div>}
            <div className="chat-bubble__body">
              {msg.typing
                ? <span className="chat-typing"><span /><span /><span /></span>
                : <span className="chat-bubble__text">{msg.text.split('\n').filter(l => l.trim()).map((line, i, arr) => (
                    <React.Fragment key={i}>{line}{i < arr.length - 1 && <br/>}</React.Fragment>
                  ))}</span>}
            </div>
          </div>
        ))}
        {isThinking && (
          <div className="chat-bubble chat-bubble--bot">
            <div className="chat-bubble__avatar">FJ</div>
            <div className="chat-bubble__body">
              <span className="chat-typing"><span /><span /><span /></span>
            </div>
          </div>
        )}
      </div>

      {canEarlySubmit && (
        <div className="chat-early-submit">
          <button className="chat-early-btn" onClick={() => {
            const suggest = inferMaturityFromChat(transcript, extraction);
            onResult({
              ...extraction,
              extracted: { ...extraction?.extracted, trackingMaturity: extraction?.extracted?.trackingMaturity || suggest.dims },
              maturitySuggestion: suggest,
            }, transcript);
          }}>
            See numbers now ↗
          </button>
          <span className="chat-early-label">defaults will be used where data is missing — less accurate</span>
        </div>
      )}

      <div className="chat-input-bar">
        {canProceed && (
          <button className="btn primary" style={{ whiteSpace: "nowrap" }} onClick={() => {
            const suggest = inferMaturityFromChat(transcript, extraction);
            onResult({
              ...extraction,
              extracted: { ...extraction?.extracted, trackingMaturity: extraction?.extracted?.trackingMaturity || suggest.dims },
              maturitySuggestion: suggest,
            }, transcript);
          }}>
            Show my numbers →
          </button>
        )}
        <input
          ref={inputRef}
          className="chat-input-field"
          placeholder={canProceed ? "Or add more context first…" : "Type your answer…"}
          value={input}
          onChange={e => setInput(e.target.value)}
          onKeyDown={e => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); handleSend(); } }}
          disabled={isThinking}
          autoFocus
        />
        <button className="chat-send-btn" onClick={handleSend} disabled={isThinking || !input.trim()}>
          →
        </button>
      </div>
    </div>
  );
}

/* ------------------------------------------------------------
   UnderstoodPanel — "Here's what we understood"
   Two-column read-out: stated (✓) and inferred (i with reasoning tooltip).
   ------------------------------------------------------------ */
function UnderstoodPanel({ extraction, userText, onTweakClick, defaultOpen = true }) {
  const [open, setOpen] = useStateCV(defaultOpen);
  const stated = (extraction?.stated || []).map((f) => ({
    field: f,
    value: extraction.extracted?.[f],
    kind: "stated",
    reasoning: null,
  }));
  const inferred = (extraction?.inferred || []).map((i) => ({
    field: i.field.split(".")[0], // "plannedCapex.price" → "plannedCapex"
    value: i.value,
    kind: "inferred",
    reasoning: i.reasoning,
  }));
  // dedup by field (stated wins), also dedup inferred entries that share a field name
  const seen = new Set(stated.map((s) => s.field));
  const seenAll = new Set();
  const all = [...stated, ...inferred.filter((i) => !seen.has(i.field))].filter((row) => {
    if (seenAll.has(row.field)) return false;
    seenAll.add(row.field);
    return true;
  });

  return (
    <div className={"understood" + (open ? "" : " understood--collapsed")}>
      <div className="understood__head">
        <div>
          <div className="eyebrow">Reading of your description</div>
          <h3 className="understood__h3">Here's what we understood</h3>
        </div>
        <div className="understood__headRight">
          <span className="understood__legend">
            <span className="understood__icon understood__icon--stated">✓</span> you said &nbsp;·&nbsp;
            <span className="understood__icon understood__icon--inferred">i</span> we inferred
          </span>
          <button className="btn link" onClick={() => setOpen((o) => !o)} style={{ fontSize: 12 }}>
            {open ? "Hide" : "Show"}
          </button>
        </div>
      </div>

      {open && (
        <>
          <div className="understood__items">
            {all.map((row) => {
              const label = FIELD_LABELS[row.field] || row.field;
              const fmt = FIELD_FORMAT[row.field];
              const valueDisplay = fmt ? fmt(row.value) : String(row.value);
              return (
                <div key={row.field} className={"understood__item understood__item--" + row.kind}>
                  <span className={"understood__icon understood__icon--" + row.kind}>
                    {row.kind === "stated" ? "✓" : "i"}
                  </span>
                  <div className="understood__pair">
                    <div className="understood__label">{label}</div>
                    <div className="understood__value">{valueDisplay}</div>
                  </div>
                  {row.reasoning && (
                    <span className="understood__why" tabIndex={0} title={row.reasoning}>
                      <span className="understood__whyTip">{row.reasoning}</span>
                    </span>
                  )}
                </div>
              );
            })}
          </div>
          <div className="understood__foot">
            <span>Anything wrong, or want to add detail?</span>
            <button className="btn link" onClick={onTweakClick}>Tweak the inputs ↓</button>
          </div>
        </>
      )}
    </div>
  );
}

/* ------------------------------------------------------------
   TellUsMore — persistent thin input that feeds an extract call back in
   ------------------------------------------------------------ */
function TellUsMore({ onAddContext }) {
  const [v, setV] = useStateCV("");
  const [busy, setBusy] = useStateCV(false);
  const submit = () => {
    if (v.trim().length < 4 || busy) return;
    setBusy(true);
    setTimeout(() => {
      onAddContext?.(v);
      setV("");
      setBusy(false);
    }, 700);
  };
  return (
    <div className="tellmore">
      <div className="tellmore__head">
        <div className="eyebrow">Continue the conversation</div>
        <h4 className="tellmore__h4">Add more context for a better result</h4>
        <p className="tellmore__p">
          Anything you didn't mention — charge-out rate, planned capex, manual data collection, OTD penalties, customer mix.
          We'll pull out what's useful and merge it with what we already have.
        </p>
      </div>
      <div className="tellmore__row">
        <input
          type="text"
          className="tellmore__input"
          value={v}
          onChange={(e) => setV(e.target.value)}
          onKeyDown={(e) => { if (e.key === "Enter") submit(); }}
          placeholder={busy ? "Reading…" : 'e.g. "Our charge-out is £55/hr and we lose ~6 hrs/week to manual job tracking."'}
          disabled={busy}
        />
        <button className="btn primary" onClick={submit} disabled={busy || v.trim().length < 4}>
          {busy ? "Reading…" : "Add"}
        </button>
      </div>
    </div>
  );
}

Object.assign(window, { ChatConversation, UnderstoodPanel, TellUsMore, regexExtract, mockExtract, nextBotQuestion, analyseOpportunity, inferMaturityFromChat, MATURITY_GROUPS });
