const {
  useState, useCallback, useEffect, useMemo, useRef
} = React;

const {
  ReactFlow,
  ReactFlowProvider,
  useNodesState,
  useEdgesState,
  useReactFlow,
  Handle,
  Position,
  MarkerType,
  addEdge,
  Background,
  Controls,
  MiniMap
} = window.ReactFlow;

// ─── Constants ─────────────────────────────────────────────────────────────

const NODE_TYPES_CONFIG = {
  process:    { bg: "#DBEAFE", border: "#3B82F6", icon: "⚙️", label: "Process" },
  queue:      { bg: "#FEF3C7", border: "#F59E0B", icon: "📦", label: "Queue" },
  inspection: { bg: "#FEE2E2", border: "#EF4444", icon: "🔍", label: "Inspection" },
  changeover: { bg: "#FCE7F3", border: "#EC4899", icon: "🔄", label: "Changeover" },
  material:   { bg: "#DCFCE7", border: "#22C55E", icon: "🏭", label: "Material" },
};

const NODE_SCHEMAS = {
  process: [
    { name: "Step Name", dataType: "text", source: "interview", confidence: "high", question: "What do you call this step? What's happening to the part here?" },
    { name: "Cycle Time", dataType: "duration", source: "interview", confidence: "medium", question: "If everything's going normally, how long from when material arrives to when it leaves?" },
    { name: "Operator Count", dataType: "integer", source: "interview", confidence: "high", question: "How many people are working at this station at the same time?" },
    { name: "Uptime / Availability", dataType: "percentage", source: "observation", confidence: "low", question: "How often does this step stop unexpectedly? What causes it?" },
    { name: "Common Failure Modes", dataType: "text_list", source: "interview", confidence: "high", question: "What's the most common reason this step slows down or stops?" },
    { name: "Output Unit Definition", dataType: "text", source: "interview", confidence: "high", question: "What does one finished piece look like when it leaves your station?" }
  ],
  queue: [
    { name: "Location", dataType: "text", source: "interview", confidence: "high", question: "Where does material sit between this step and the next?" },
    { name: "Typical WIP Volume", dataType: "count", source: "observation", confidence: "low", question: "How many pieces are usually sitting there waiting?" },
    { name: "Wait Time", dataType: "duration", source: "observation", confidence: "low", question: "How long does a part usually wait here before moving forward?" },
    { name: "Wait Cause", dataType: "text", source: "interview", confidence: "high", question: "Why does material pile up here — is the next step slower, or something else?" },
    { name: "FIFO / LIFO Discipline", dataType: "enum", source: "interview", confidence: "high", question: "When you pick up material, do you take from the top, the bottom, or wherever is easiest?" }
  ],
  inspection: [
    { name: "What Is Being Checked", dataType: "text_list", source: "interview", confidence: "high", question: "What are you looking for? What would make you reject a piece?" },
    { name: "Inspection Method", dataType: "text", source: "interview", confidence: "high", question: "How do you check it — by eye, with a tool, a gauge, a machine?" },
    { name: "Defect / Rejection Rate", dataType: "percentage", source: "records", confidence: "low", question: "Out of 100 pieces, roughly how many get rejected?" },
    { name: "Rework Path", dataType: "text", source: "interview", confidence: "high", question: "When something gets rejected, where does it go? Is it fixed here, sent back, or scrapped?" },
    { name: "Inspection Cycle Time", dataType: "duration", source: "observation", confidence: "medium", question: "How long does it take to check one piece?" }
  ],
  changeover: [
    { name: "Changeover Trigger", dataType: "text", source: "interview", confidence: "high", question: "What causes this station to switch over — different product, size, material?" },
    { name: "Estimated Duration", dataType: "duration", source: "observation", confidence: "low", question: "How long does it take to switch over?" },
    { name: "Changeover Frequency", dataType: "frequency", source: "interview", confidence: "high", question: "How many times per shift does this switchover happen?" },
    { name: "Internal vs External Tasks", dataType: "text_list", source: "interview", confidence: "high", question: "While the machine is stopped, what's happening? Are people gathering tools or adjusting the machine itself?" },
    { name: "Tooling Required", dataType: "text_list", source: "interview", confidence: "high", question: "What tools or materials do you need ready before the changeover can start?" },
    { name: "First-Pass Quality", dataType: "count", source: "records", confidence: "low", question: "How many pieces do you run after a changeover before you're confident the output is good?" },
    { name: "Minimum Batch Size", dataType: "count", source: "interview", confidence: "high", question: "What's the smallest run you'd want before switching over again?" }
  ],
  material: [
    { name: "Supplier / Source", dataType: "text", source: "interview", confidence: "high", question: "Where does this material come from — outside supplier, warehouse, or another part of the facility?" },
    { name: "Delivery Frequency", dataType: "frequency", source: "records", confidence: "medium", question: "How often does this material arrive?" },
    { name: "Lead Time", dataType: "duration", source: "interview", confidence: "medium", question: "If you ran out today and ordered more, how long before it arrives?" },
    { name: "Incoming Quality Issues", dataType: "text", source: "interview", confidence: "high", question: "Does this material ever arrive in a condition that causes problems downstream?" },
    { name: "Safety Stock Level", dataType: "count", source: "observation", confidence: "medium", question: "How much of this material is usually on hand at any given time?" }
  ]
};

// ─── Helpers ────────────────────────────────────────────────────────────────

function makeNodeId() {
  return "node_" + Date.now() + "_" + Math.random().toString(36).slice(2, 7);
}

function makeEdgeId() {
  return "edge_" + Date.now() + "_" + Math.random().toString(36).slice(2, 7);
}

function buildDefaultFields(nodeType) {
  const schema = NODE_SCHEMAS[nodeType] || [];
  const fields = {};
  schema.forEach(f => {
    fields[f.name] = { value: "", source: f.source, confidence: f.confidence, verified: false };
  });
  return fields;
}

function createNode(nodeType, position) {
  const cfg = NODE_TYPES_CONFIG[nodeType];
  return {
    id: makeNodeId(),
    type: nodeType,
    position,
    data: {
      nodeType,
      label: cfg.label + " " + (Math.floor(Math.random() * 90) + 10),
      fields: buildDefaultFields(nodeType),
      interviewHistory: [],
      dismissedPrompts: []
    }
  };
}

function parseDurationToMinutes(val) {
  if (!val) return null;
  const s = String(val).toLowerCase().trim();
  const num = parseFloat(s);
  if (isNaN(num)) return null;
  if (s.includes("hour") || s.includes("hr")) return num * 60;
  if (s.includes("sec")) return num / 60;
  return num; // assume minutes
}

function getNodeStatus(nodeData) {
  const schemaFields = NODE_SCHEMAS[nodeData.nodeType] || [];
  const interviewFields = schemaFields.filter(f => f.source === "interview" && f.confidence === "high");
  if (interviewFields.length === 0) return "green";
  const filled = interviewFields.filter(f => {
    const v = nodeData.fields[f.name]?.value;
    return v !== undefined && v !== null && String(v).trim() !== "";
  });
  if (filled.length === 0) return "red";
  if (filled.length === interviewFields.length) return "green";
  return "amber";
}

function extractJSON(text) {
  const stripped = text.replace(/```json\n?/g, "").replace(/```\n?/g, "");
  const match = stripped.match(/\{[\s\S]*\}/);
  if (!match) return null;
  try { return JSON.parse(match[0]); } catch { return null; }
}

// ─── Discovery helpers ──────────────────────────────────────────────────────

const MODEL_OPTIONS = [
  { id: "nvidia/nemotron-3-nano-30b-a3b:free", label: "Nemotron Nano (Free)" },
  { id: "google/gemini-2.0-flash-001", label: "Gemini 2.0 Flash" },
  { id: "anthropic/claude-3.5-haiku", label: "Claude 3.5 Haiku" },
  { id: "openai/gpt-4o-mini", label: "GPT-4o Mini" },
];

function normalizeNodeType(rawType) {
  const t = (rawType || "").toLowerCase().trim();
  if (t.includes("queue") || t.includes("wait") || t.includes("buffer")) return "queue";
  if (t.includes("inspect") || t.includes("check") || t.includes("quality")) return "inspection";
  if (t.includes("change") || t.includes("setup") || t.includes("switch")) return "changeover";
  if (t.includes("material") || t.includes("supply") || t.includes("raw") || t.includes("input")) return "material";
  return "process";
}

function generateDiagramFromDiscovery(jsonData) {
  const rawSteps = jsonData.process_steps || jsonData.nodes || [];
  if (rawSteps.length === 0) return null;

  const X_SPACING = 280;
  const Y_MATERIAL = 100;
  const Y_MAIN = 300;
  const Y_SECONDARY = 500;

  // Separate by type for lane-based positioning
  let mainIndex = 0;
  let matIndex = 0;
  let secIndex = 0;

  const rfNodes = [];
  const idMap = {};  // step id/index -> nodeId

  rawSteps.forEach((raw, i) => {
    const nodeType = normalizeNodeType(raw.type);
    const nodeId = makeNodeId();
    const stepKey = raw.id || String(i);
    idMap[stepKey] = nodeId;
    // Also map by index
    idMap[String(i)] = nodeId;

    let position;
    if (nodeType === "material") {
      position = { x: matIndex * X_SPACING, y: Y_MATERIAL };
      matIndex++;
    } else if (nodeType === "inspection" || nodeType === "changeover") {
      position = { x: secIndex * X_SPACING, y: Y_SECONDARY };
      secIndex++;
    } else {
      position = { x: mainIndex * X_SPACING, y: Y_MAIN };
      mainIndex++;
    }

    const fields = buildDefaultFields(nodeType);
    // Merge details from LLM
    if (raw.details) {
      const schema = NODE_SCHEMAS[nodeType] || [];
      Object.entries(raw.details).forEach(([key, val]) => {
        if (val === null || val === undefined || String(val).trim() === "") return;
        // Exact match first
        if (fields[key] !== undefined) {
          fields[key] = { ...fields[key], value: val };
          return;
        }
        // Fuzzy match
        const match = schema.find(f =>
          f.name.toLowerCase() === key.toLowerCase() ||
          f.name.toLowerCase().includes(key.toLowerCase()) ||
          key.toLowerCase().includes(f.name.toLowerCase().split(" ")[0])
        );
        if (match) {
          fields[match.name] = { ...fields[match.name], value: val };
        }
      });
    }

    // Set name into the appropriate label field
    if (raw.name) {
      const nameField = nodeType === "process" ? "Step Name" :
                        nodeType === "queue" ? "Location" :
                        nodeType === "inspection" ? "What Is Being Checked" :
                        nodeType === "changeover" ? "Changeover Trigger" :
                        nodeType === "material" ? "Supplier / Source" : null;
      if (nameField && fields[nameField] && !fields[nameField].value) {
        fields[nameField] = { ...fields[nameField], value: raw.name };
      }
    }

    const cfg = NODE_TYPES_CONFIG[nodeType];
    rfNodes.push({
      id: nodeId,
      type: nodeType,
      position,
      data: {
        nodeType,
        label: raw.name || (cfg.label + " " + (i + 1)),
        fields,
        interviewHistory: [],
        dismissedPrompts: []
      }
    });
  });

  // Build edges
  const rfEdges = [];
  const addedEdges = new Set();

  // From explicit edges array
  if (jsonData.edges && Array.isArray(jsonData.edges)) {
    jsonData.edges.forEach(e => {
      const srcKey = String(e.from_index !== undefined ? e.from_index : e.from);
      const tgtKey = String(e.to_index !== undefined ? e.to_index : e.to);
      const sourceId = idMap[srcKey];
      const targetId = idMap[tgtKey];
      if (sourceId && targetId) {
        const key = sourceId + "->" + targetId;
        if (!addedEdges.has(key)) {
          addedEdges.add(key);
          rfEdges.push({
            id: makeEdgeId(), source: sourceId, target: targetId,
            markerEnd: { type: MarkerType.ArrowClosed }, style: { strokeWidth: 2 }
          });
        }
      }
    });
  }

  // From connects_from in process_steps format
  if (rfEdges.length === 0) {
    rawSteps.forEach((raw, i) => {
      const targetId = idMap[raw.id || String(i)];
      (raw.connects_from || []).forEach(fromRef => {
        const sourceId = idMap[String(fromRef)];
        if (sourceId && targetId) {
          const key = sourceId + "->" + targetId;
          if (!addedEdges.has(key)) {
            addedEdges.add(key);
            rfEdges.push({
              id: makeEdgeId(), source: sourceId, target: targetId,
              markerEnd: { type: MarkerType.ArrowClosed }, style: { strokeWidth: 2 }
            });
          }
        }
      });
    });
  }

  // Fallback: if still no edges and all main-flow nodes, chain them sequentially
  if (rfEdges.length === 0 && rfNodes.length > 1) {
    const mainFlowNodes = rfNodes.filter(n =>
      n.data.nodeType === "process" || n.data.nodeType === "queue"
    );
    for (let i = 0; i < mainFlowNodes.length - 1; i++) {
      rfEdges.push({
        id: makeEdgeId(), source: mainFlowNodes[i].id, target: mainFlowNodes[i + 1].id,
        markerEnd: { type: MarkerType.ArrowClosed }, style: { strokeWidth: 2 }
      });
    }
  }

  return { nodes: rfNodes, edges: rfEdges };
}

function buildDiscoverySystemPrompt() {
  return `You are a friendly process documentation assistant helping a manufacturing plant employee describe their production process so it can be mapped visually.

YOUR GOAL: Understand their manufacturing process well enough to create a process flow diagram. You need to identify each step, what happens between steps, quality checks, equipment changeovers, and material inputs.

CONVERSATION STRATEGY:
1. First, understand what they make and which product line to map.
2. Ask them to walk through the process from start to finish at a high level.
3. Then go back through each step and ask clarifying questions:
   - What exactly happens at each step?
   - How long does each step take and how many people work there?
   - Are there any waits or pile-ups of material between steps?
   - Are there any quality checks or inspections?
   - Does any equipment need to be switched over between product types?
   - What raw materials or components feed into the process?
4. For each step, try to learn approximate timing, staffing, and common problems.

RULES:
- Ask ONE question at a time. Keep questions short and conversational.
- Never use technical lean/manufacturing jargon (no "VSM", "takt time", "WIP", "SMED", "kaizen", "muda", "kanban", "poka-yoke").
- Use plain language: "wait", "pile-up", "check", "switch over", "bottleneck".
- Be encouraging and acknowledge what the user tells you.
- Build up your understanding progressively. After each user reply, update your running model of the process.

AFTER EACH USER MESSAGE, you MUST append a JSON code block (hidden from the user) with your current process model:

\`\`\`json
{
  "process_steps": [
    {
      "id": "step_1",
      "type": "process",
      "name": "Descriptive step name",
      "connects_from": [],
      "details": {}
    }
  ],
  "ready_to_generate": false
}
\`\`\`

STEP TYPES (use exactly these):
- "process" — a value-adding work step
- "queue" — a wait, buffer, or pile-up between steps
- "inspection" — a quality check or test
- "changeover" — equipment switching between products
- "material" — a raw material or component input

DETAILS FIELDS by type (use these exact keys when you have data):
- process: "Step Name", "Cycle Time", "Operator Count", "Uptime / Availability", "Common Failure Modes", "Output Unit Definition"
- queue: "Location", "Typical WIP Volume", "Wait Time", "Wait Cause", "FIFO / LIFO Discipline"
- inspection: "What Is Being Checked", "Inspection Method", "Defect / Rejection Rate", "Rework Path", "Inspection Cycle Time"
- changeover: "Changeover Trigger", "Estimated Duration", "Changeover Frequency", "Internal vs External Tasks", "Tooling Required", "First-Pass Quality", "Minimum Batch Size"
- material: "Supplier / Source", "Delivery Frequency", "Lead Time", "Incoming Quality Issues", "Safety Stock Level"

CONNECTIONS: Use "connects_from" to show which step(s) feed into each step. Use the step id values. The first step has an empty array.

Set "ready_to_generate" to true ONLY when ALL of these are met:
- You have identified at least 3 process steps
- You have a clear flow from start to finish
- You have asked about waits/queues between major steps
- You have asked about quality checks
- The user seems to have described the full process

When ready_to_generate is true, tell the user something like: "Great, I think I have a good picture of your process! I've identified [N] steps from [first step] to [last step]. Click 'Generate Map' whenever you're ready, or keep telling me more details."`;
}

// ─── Toast Component ────────────────────────────────────────────────────────

function ToastContainer({ toasts, onDismiss }) {
  return (
    <div style={{ position: "fixed", bottom: 24, left: "50%", transform: "translateX(-50%)", zIndex: 9999, display: "flex", flexDirection: "column-reverse", gap: 8, alignItems: "center", pointerEvents: "none" }}>
      {toasts.map(t => (
        <div key={t.id} className="toast-enter" style={{ pointerEvents: "auto", background: "white", border: "1px solid #e5e7eb", borderRadius: 12, boxShadow: "0 4px 24px rgba(0,0,0,0.12)", padding: "12px 16px", minWidth: 280, maxWidth: 420, display: "flex", alignItems: "flex-start", gap: 12 }}>
          <div style={{ flex: 1, fontSize: 14, color: "#374151" }}>{t.message}</div>
          <div style={{ display: "flex", gap: 6, alignItems: "center", flexShrink: 0 }}>
            {t.actions && t.actions.map((a, i) => (
              <button key={i} onClick={() => { a.onClick(); onDismiss(t.id); }} style={{ background: "#3B82F6", color: "white", border: "none", borderRadius: 6, padding: "4px 10px", fontSize: 12, cursor: "pointer", whiteSpace: "nowrap" }}>
                {a.label}
              </button>
            ))}
            <button onClick={() => onDismiss(t.id)} style={{ background: "none", border: "none", color: "#9CA3AF", cursor: "pointer", fontSize: 16, lineHeight: 1, padding: 2 }}>✕</button>
          </div>
        </div>
      ))}
    </div>
  );
}

// ─── Settings Modal ─────────────────────────────────────────────────────────

function SettingsModal({ apiKey, modelId, onSave, onClose }) {
  const [key, setKey] = useState(apiKey);
  const [model, setModel] = useState(modelId);
  const [customModel, setCustomModel] = useState(
    MODEL_OPTIONS.some(m => m.id === modelId) ? "" : modelId
  );
  const isCustom = !MODEL_OPTIONS.some(m => m.id === model);

  function handleSave() {
    onSave(key, isCustom && customModel ? customModel : model);
    onClose();
  }

  return (
    <div style={{ position: "fixed", inset: 0, background: "rgba(0,0,0,0.5)", zIndex: 10000, display: "flex", alignItems: "center", justifyContent: "center" }} onClick={onClose}>
      <div style={{ background: "white", borderRadius: 16, padding: 32, width: 440, maxWidth: "90vw", boxShadow: "0 20px 60px rgba(0,0,0,0.3)" }} onClick={e => e.stopPropagation()}>
        <h2 style={{ margin: "0 0 8px", fontSize: 20, fontWeight: 700 }}>Settings</h2>
        <p style={{ margin: "0 0 20px", fontSize: 14, color: "#6B7280" }}>
          Configure your OpenRouter API key and model to enable AI features. Your key is stored only in browser memory.
        </p>

        <label style={{ display: "block", fontSize: 13, fontWeight: 600, color: "#374151", marginBottom: 6 }}>OpenRouter API Key</label>
        <input
          type="password"
          value={key}
          onChange={e => setKey(e.target.value)}
          placeholder="sk-or-..."
          style={{ width: "100%", border: "1px solid #d1d5db", borderRadius: 8, padding: "10px 12px", fontSize: 14, boxSizing: "border-box", outline: "none" }}
        />
        <p style={{ margin: "8px 0 16px", fontSize: 12, color: "#9CA3AF" }}>
          Get a free key at openrouter.ai
        </p>

        <label style={{ display: "block", fontSize: 13, fontWeight: 600, color: "#374151", marginBottom: 6 }}>Model</label>
        <select
          value={MODEL_OPTIONS.some(m => m.id === model) ? model : "__custom__"}
          onChange={e => {
            if (e.target.value === "__custom__") {
              setModel("__custom__");
            } else {
              setModel(e.target.value);
              setCustomModel("");
            }
          }}
          style={{ width: "100%", border: "1px solid #d1d5db", borderRadius: 8, padding: "10px 12px", fontSize: 14, boxSizing: "border-box", outline: "none", background: "white", cursor: "pointer" }}
        >
          {MODEL_OPTIONS.map(m => (
            <option key={m.id} value={m.id}>{m.label}</option>
          ))}
          <option value="__custom__">Custom model...</option>
        </select>
        {(model === "__custom__" || isCustom) && (
          <input
            type="text"
            value={customModel}
            onChange={e => { setCustomModel(e.target.value); setModel("__custom__"); }}
            placeholder="e.g. meta-llama/llama-3-70b"
            style={{ width: "100%", border: "1px solid #d1d5db", borderRadius: 8, padding: "10px 12px", fontSize: 14, boxSizing: "border-box", outline: "none", marginTop: 8 }}
          />
        )}
        <p style={{ margin: "8px 0 20px", fontSize: 12, color: "#9CA3AF" }}>
          Free tier: Nemotron Nano. For better results in Discovery, try a paid model.
        </p>

        <div style={{ display: "flex", gap: 10, justifyContent: "flex-end" }}>
          <button onClick={onClose} style={{ border: "1px solid #d1d5db", background: "white", borderRadius: 8, padding: "8px 16px", fontSize: 14, cursor: "pointer" }}>Cancel</button>
          <button onClick={handleSave} style={{ background: "#3B82F6", color: "white", border: "none", borderRadius: 8, padding: "8px 16px", fontSize: 14, cursor: "pointer", fontWeight: 600 }}>Save</button>
        </div>
      </div>
    </div>
  );
}

// ─── Custom Node Components ─────────────────────────────────────────────────

function CustomNodeBase({ data, selected, isBottleneck }) {
  const cfg = NODE_TYPES_CONFIG[data.nodeType] || NODE_TYPES_CONFIG.process;
  const status = getNodeStatus(data);
  const statusColor = status === "green" ? "#22C55E" : status === "amber" ? "#F59E0B" : "#EF4444";

  return (
    <div style={{
      background: cfg.bg,
      border: `2px solid ${isBottleneck ? "#EF4444" : cfg.border}`,
      borderRadius: 12,
      minWidth: 160,
      padding: "10px 14px",
      boxShadow: isBottleneck ? "0 0 0 3px #FCA5A5" : "0 2px 8px rgba(0,0,0,0.08)",
      position: "relative",
      cursor: "pointer",
      transition: "box-shadow 0.2s",
      outline: selected ? `3px solid ${cfg.border}` : "none",
      outlineOffset: 2
    }}>
      <Handle type="target" position={Position.Left} style={{ background: cfg.border, width: 10, height: 10 }} />
      <Handle type="source" position={Position.Right} style={{ background: cfg.border, width: 10, height: 10 }} />

      <div style={{ position: "absolute", top: 8, right: 8, width: 10, height: 10, borderRadius: "50%", background: statusColor, border: "2px solid white", boxShadow: "0 1px 3px rgba(0,0,0,0.2)" }} />

      {isBottleneck && (
        <div style={{ position: "absolute", top: -12, left: "50%", transform: "translateX(-50%)", fontSize: 16, lineHeight: 1 }}>⚠️</div>
      )}

      <div style={{ display: "flex", alignItems: "center", gap: 8, paddingRight: 16 }}>
        <span style={{ fontSize: 20 }}>{cfg.icon}</span>
        <div>
          <div style={{ fontSize: 11, color: cfg.border, fontWeight: 600, textTransform: "uppercase", letterSpacing: "0.05em" }}>{cfg.label}</div>
          <div style={{ fontSize: 13, fontWeight: 700, color: "#111827", marginTop: 1 }}>{data.label}</div>
        </div>
      </div>

      {data.nodeType === "process" && data.fields["Cycle Time"]?.value && (
        <div style={{ marginTop: 6, fontSize: 11, color: "#6B7280" }}>
          CT: {data.fields["Cycle Time"].value}
        </div>
      )}
      {data.nodeType === "queue" && data.fields["Wait Time"]?.value && (
        <div style={{ marginTop: 6, fontSize: 11, color: "#6B7280" }}>
          Wait: {data.fields["Wait Time"].value}
        </div>
      )}
    </div>
  );
}

// We need a way to pass bottleneck info into nodes. We'll use a context.
const BottleneckContext = React.createContext(new Set());

function ProcessNode(props) {
  const bottleneckIds = React.useContext(BottleneckContext);
  return <CustomNodeBase {...props} isBottleneck={bottleneckIds.has(props.id)} />;
}
function QueueNode(props) { return <CustomNodeBase {...props} isBottleneck={false} />; }
function InspectionNode(props) { return <CustomNodeBase {...props} isBottleneck={false} />; }
function ChangeoverNode(props) { return <CustomNodeBase {...props} isBottleneck={false} />; }
function MaterialNode(props) { return <CustomNodeBase {...props} isBottleneck={false} />; }

const nodeTypes = {
  process: ProcessNode,
  queue: QueueNode,
  inspection: InspectionNode,
  changeover: ChangeoverNode,
  material: MaterialNode,
};

// ─── Right Panel ────────────────────────────────────────────────────────────

function SourceBadge({ source }) {
  const colors = { interview: ["#DBEAFE", "#1D4ED8"], observation: ["#FEF3C7", "#92400E"], records: ["#F3E8FF", "#6B21A8"] };
  const [bg, text] = colors[source] || ["#F3F4F6", "#374151"];
  return <span style={{ background: bg, color: text, borderRadius: 20, padding: "2px 8px", fontSize: 11, fontWeight: 600 }}>{source}</span>;
}

function ConfidenceBadge({ confidence }) {
  const colors = { high: ["#DCFCE7", "#15803D"], medium: ["#FEF3C7", "#92400E"], low: ["#FEE2E2", "#991B1B"] };
  const [bg, text] = colors[confidence] || ["#F3F4F6", "#374151"];
  return <span style={{ background: bg, color: text, borderRadius: 20, padding: "2px 8px", fontSize: 11, fontWeight: 600 }}>{confidence}</span>;
}

function ManualEditTab({ node, onChange }) {
  const schema = NODE_SCHEMAS[node.data.nodeType] || [];
  const [localLabel, setLocalLabel] = useState(node.data.label);

  useEffect(() => { setLocalLabel(node.data.label); }, [node.id]);

  function handleFieldChange(fieldName, key, val) {
    const newFields = { ...node.data.fields, [fieldName]: { ...node.data.fields[fieldName], [key]: val } };
    onChange({ fields: newFields });
  }

  function handleLabelBlur() {
    if (localLabel !== node.data.label) onChange({ label: localLabel });
  }

  return (
    <div style={{ padding: "16px", overflowY: "auto", flex: 1 }}>
      <div style={{ marginBottom: 20 }}>
        <label style={{ display: "block", fontSize: 12, fontWeight: 700, color: "#374151", marginBottom: 6, textTransform: "uppercase", letterSpacing: "0.05em" }}>Node Label</label>
        <input
          value={localLabel}
          onChange={e => setLocalLabel(e.target.value)}
          onBlur={handleLabelBlur}
          style={{ width: "100%", border: "1px solid #d1d5db", borderRadius: 8, padding: "8px 10px", fontSize: 14, boxSizing: "border-box" }}
        />
      </div>

      {schema.map(field => {
        const fieldData = node.data.fields[field.name] || { value: "", source: field.source, confidence: field.confidence, verified: false };
        const isLowConf = field.confidence === "low";

        return (
          <div key={field.name} style={{ marginBottom: 20, padding: 14, background: "#F9FAFB", borderRadius: 10, border: "1px solid #E5E7EB" }}>
            <div style={{ display: "flex", alignItems: "flex-start", justifyContent: "space-between", marginBottom: 6 }}>
              <span style={{ fontWeight: 700, fontSize: 13, color: "#111827" }}>{field.name}</span>
              <div style={{ display: "flex", gap: 4 }}>
                <SourceBadge source={field.source} />
                <ConfidenceBadge confidence={field.confidence} />
              </div>
            </div>

            <p style={{ fontSize: 11, color: "#9CA3AF", margin: "0 0 8px", fontStyle: "italic" }}>{field.question}</p>

            {field.dataType === "text_list" ? (
              <textarea
                value={fieldData.value}
                onChange={e => handleFieldChange(field.name, "value", e.target.value)}
                rows={3}
                placeholder="Enter values, one per line..."
                style={{ width: "100%", border: "1px solid #d1d5db", borderRadius: 6, padding: "6px 8px", fontSize: 13, boxSizing: "border-box", resize: "vertical" }}
              />
            ) : field.dataType === "integer" || field.dataType === "count" ? (
              <input
                type="number"
                value={fieldData.value}
                onChange={e => handleFieldChange(field.name, "value", e.target.value)}
                style={{ width: "100%", border: "1px solid #d1d5db", borderRadius: 6, padding: "6px 8px", fontSize: 13, boxSizing: "border-box" }}
              />
            ) : field.dataType === "percentage" ? (
              <div style={{ display: "flex", alignItems: "center", gap: 6 }}>
                <input
                  type="number" min="0" max="100"
                  value={fieldData.value}
                  onChange={e => handleFieldChange(field.name, "value", e.target.value)}
                  style={{ flex: 1, border: "1px solid #d1d5db", borderRadius: 6, padding: "6px 8px", fontSize: 13 }}
                />
                <span style={{ color: "#6B7280", fontSize: 13 }}>%</span>
              </div>
            ) : field.dataType === "enum" ? (
              <select
                value={fieldData.value}
                onChange={e => handleFieldChange(field.name, "value", e.target.value)}
                style={{ width: "100%", border: "1px solid #d1d5db", borderRadius: 6, padding: "6px 8px", fontSize: 13, background: "white" }}
              >
                <option value="">Select...</option>
                <option value="FIFO">FIFO (First In, First Out)</option>
                <option value="LIFO">LIFO (Last In, First Out)</option>
                <option value="Random">Random / No discipline</option>
                <option value="Priority">Priority-based</option>
              </select>
            ) : (
              <input
                type="text"
                value={fieldData.value}
                onChange={e => handleFieldChange(field.name, "value", e.target.value)}
                placeholder={field.dataType === "duration" ? "e.g. 5 minutes, 30 seconds, 2 hours" : field.dataType === "frequency" ? "e.g. 3 times/shift" : ""}
                style={{ width: "100%", border: "1px solid #d1d5db", borderRadius: 6, padding: "6px 8px", fontSize: 13, boxSizing: "border-box" }}
              />
            )}

            <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginTop: 8 }}>
              <label style={{ display: "flex", alignItems: "center", gap: 6, fontSize: 12, color: "#6B7280", cursor: "pointer" }}>
                <input
                  type="checkbox"
                  checked={fieldData.verified || false}
                  onChange={e => handleFieldChange(field.name, "verified", e.target.checked)}
                />
                Verified
              </label>
              {isLowConf && !fieldData.verified && (
                <span style={{ fontSize: 11, color: "#DC2626", fontWeight: 500 }}>⚠ Verify with records/observation</span>
              )}
            </div>
          </div>
        );
      })}
    </div>
  );
}

function InterviewTab({ node, apiKey, modelId, onChange, addToast }) {
  const [inputText, setInputText] = useState("");
  const [loading, setLoading] = useState(false);
  const chatEndRef = useRef(null);

  const history = node.data.interviewHistory || [];

  useEffect(() => {
    chatEndRef.current?.scrollIntoView({ behavior: "smooth" });
  }, [history]);

  function buildSystemPrompt() {
    const schema = NODE_SCHEMAS[node.data.nodeType] || [];
    const fieldList = schema.map(f => f.name).join(", ");
    const populated = schema
      .filter(f => node.data.fields[f.name]?.value?.toString().trim() !== "")
      .map(f => `${f.name}: ${node.data.fields[f.name].value}`)
      .join("; ");

    return `You are a process documentation assistant helping plant employees describe their manufacturing process. Your goal is to understand each step in plain language and extract structured data.

Current node type: ${node.data.nodeType}
Fields to populate: ${fieldList}
Fields already populated: ${populated || "none"}

Rules:
- Ask one question at a time in plain, non-technical language.
- Never use lean manufacturing terminology (VSM, takt time, WIP, SMED, kaizen).
- After each user response, output a JSON block in this exact format:
  {"extracted": {"field_name": "value", ...}, "next_question": "..."}
- If a response is ambiguous, ask a follow-up before extracting.
- When all fields have been attempted, output: {"extracted": {...}, "complete": true}
- Never tell the user what fields you are trying to fill.`;
  }

  async function sendMessage() {
    if (!inputText.trim()) return;
    if (!apiKey) {
      addToast({ message: "Please set your API key in Settings first.", id: makeNodeId() });
      return;
    }

    const userMsg = { role: "user", content: inputText.trim() };
    const newHistory = [...history, userMsg];
    onChange({ interviewHistory: newHistory });
    setInputText("");
    setLoading(true);

    try {
      const systemPrompt = buildSystemPrompt();
      const response = await fetch("https://openrouter.ai/api/v1/chat/completions", {
        method: "POST",
        headers: {
          "Authorization": `Bearer ${apiKey}`,
          "HTTP-Referer": "process-map-builder",
          "Content-Type": "application/json"
        },
        body: JSON.stringify({
          model: modelId || "nvidia/nemotron-3-nano-30b-a3b:free",
          messages: [{ role: "system", content: systemPrompt }, ...newHistory]
        })
      });

      if (!response.ok) {
        const errText = await response.text();
        throw new Error(`API error ${response.status}: ${errText}`);
      }

      const data = await response.json();
      const assistantContent = data.choices?.[0]?.message?.content || "";
      const assistantMsg = { role: "assistant", content: assistantContent };
      const finalHistory = [...newHistory, assistantMsg];

      // Try to extract JSON
      const parsed = extractJSON(assistantContent);
      let updatedFields = { ...node.data.fields };

      if (parsed?.extracted) {
        const schema = NODE_SCHEMAS[node.data.nodeType] || [];
        Object.entries(parsed.extracted).forEach(([key, val]) => {
          // Find matching field (case-insensitive, partial match)
          const match = schema.find(f => f.name.toLowerCase() === key.toLowerCase() || f.name.toLowerCase().includes(key.toLowerCase()) || key.toLowerCase().includes(f.name.toLowerCase().split(" ")[0]));
          if (match) {
            updatedFields[match.name] = { ...updatedFields[match.name], value: val };
          }
        });
      }

      onChange({ interviewHistory: finalHistory, fields: updatedFields });
    } catch (err) {
      const errMsg = { role: "assistant", content: `Sorry, I encountered an error: ${err.message}` };
      onChange({ interviewHistory: [...newHistory, errMsg] });
    } finally {
      setLoading(false);
    }
  }

  function startInterview() {
    if (!apiKey) {
      addToast({ message: "Please set your API key in Settings first.", id: makeNodeId() });
      return;
    }
    const schema = NODE_SCHEMAS[node.data.nodeType] || [];
    const firstField = schema[0];
    if (!firstField) return;
    const firstQuestion = firstField.question;
    const assistantMsg = { role: "assistant", content: firstQuestion };
    onChange({ interviewHistory: [assistantMsg] });
  }

  function clearInterview() {
    onChange({ interviewHistory: [] });
  }

  return (
    <div style={{ display: "flex", flexDirection: "column", flex: 1, overflow: "hidden" }}>
      {!apiKey && (
        <div style={{ background: "#FEF3C7", border: "1px solid #F59E0B", borderRadius: 8, padding: "10px 14px", margin: "12px 16px 0", fontSize: 13, color: "#92400E" }}>
          No API key set. Go to Settings (⚙️) to add your OpenRouter key.
        </div>
      )}

      <div style={{ flex: 1, overflowY: "auto", padding: "16px", display: "flex", flexDirection: "column", gap: 12 }}>
        {history.length === 0 ? (
          <div style={{ textAlign: "center", color: "#9CA3AF", fontSize: 14, marginTop: 40 }}>
            <div style={{ fontSize: 32, marginBottom: 12 }}>💬</div>
            <p>Start an interview to fill in this node's fields through a conversation.</p>
            <button
              onClick={startInterview}
              style={{ background: "#3B82F6", color: "white", border: "none", borderRadius: 8, padding: "10px 20px", fontSize: 14, cursor: "pointer", fontWeight: 600, marginTop: 12 }}
            >
              Start Interview
            </button>
          </div>
        ) : (
          <>
            {history.map((msg, i) => {
              const isUser = msg.role === "user";
              // Show display content: strip JSON blocks from assistant messages
              let displayContent = msg.content;
              if (!isUser) {
                displayContent = msg.content.replace(/```json[\s\S]*?```/g, "").replace(/\{[\s\S]*?"extracted"[\s\S]*?\}/g, "").trim();
                if (!displayContent) displayContent = "[Data extracted]";
              }
              return (
                <div key={i} style={{ display: "flex", justifyContent: isUser ? "flex-end" : "flex-start" }}>
                  <div className={isUser ? "chat-bubble-user" : "chat-bubble-assistant"} style={{ maxWidth: "80%", padding: "10px 14px", fontSize: 13, lineHeight: 1.5 }}>
                    {displayContent}
                  </div>
                </div>
              );
            })}
            {loading && (
              <div style={{ display: "flex", justifyContent: "flex-start" }}>
                <div className="chat-bubble-assistant" style={{ padding: "10px 14px" }}>
                  <span className="spinning" style={{ display: "inline-block", fontSize: 16 }}>⟳</span>
                </div>
              </div>
            )}
            <div ref={chatEndRef} />
          </>
        )}
      </div>

      {history.length > 0 && (
        <div style={{ padding: "12px 16px", borderTop: "1px solid #E5E7EB" }}>
          <div style={{ display: "flex", gap: 8, marginBottom: 8 }}>
            <input
              value={inputText}
              onChange={e => setInputText(e.target.value)}
              onKeyDown={e => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); sendMessage(); } }}
              placeholder="Type your answer..."
              disabled={loading}
              style={{ flex: 1, border: "1px solid #d1d5db", borderRadius: 8, padding: "8px 12px", fontSize: 13, outline: "none" }}
            />
            <button
              onClick={sendMessage}
              disabled={loading || !inputText.trim()}
              style={{ background: loading || !inputText.trim() ? "#9CA3AF" : "#3B82F6", color: "white", border: "none", borderRadius: 8, padding: "8px 14px", fontSize: 13, cursor: loading || !inputText.trim() ? "not-allowed" : "pointer", fontWeight: 600 }}
            >
              Send
            </button>
          </div>
          <button onClick={clearInterview} style={{ fontSize: 11, color: "#9CA3AF", background: "none", border: "none", cursor: "pointer", padding: 0 }}>Clear conversation</button>
        </div>
      )}
    </div>
  );
}

function NodeDetailPanel({ node, apiKey, modelId, onUpdate, addToast, onClose }) {
  const [activeTab, setActiveTab] = useState("manual");
  const cfg = NODE_TYPES_CONFIG[node.data.nodeType] || NODE_TYPES_CONFIG.process;

  function handleChange(patch) {
    onUpdate(node.id, patch);
  }

  return (
    <div style={{ width: 320, background: "white", borderLeft: "1px solid #E5E7EB", display: "flex", flexDirection: "column", height: "100%", flexShrink: 0 }}>
      <div style={{ padding: "16px", borderBottom: "1px solid #E5E7EB", flexShrink: 0 }}>
        <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
          <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
            <span style={{ fontSize: 22 }}>{cfg.icon}</span>
            <div>
              <div style={{ fontSize: 11, color: cfg.border, fontWeight: 700, textTransform: "uppercase" }}>{cfg.label}</div>
              <div style={{ fontSize: 15, fontWeight: 700, color: "#111827" }}>{node.data.label}</div>
            </div>
          </div>
          <button onClick={onClose} style={{ background: "none", border: "none", color: "#9CA3AF", cursor: "pointer", fontSize: 18, padding: 4 }}>✕</button>
        </div>

        <div style={{ display: "flex", gap: 4, marginTop: 14 }}>
          {["manual", "interview"].map(tab => (
            <button
              key={tab}
              onClick={() => setActiveTab(tab)}
              style={{
                flex: 1, padding: "7px 0", borderRadius: 8, border: "none", cursor: "pointer", fontSize: 13, fontWeight: 600,
                background: activeTab === tab ? cfg.border : "#F3F4F6",
                color: activeTab === tab ? "white" : "#6B7280",
                transition: "all 0.15s"
              }}
            >
              {tab === "manual" ? "Manual Edit" : "Interview Mode"}
            </button>
          ))}
        </div>
      </div>

      <div style={{ flex: 1, overflow: "hidden", display: "flex", flexDirection: "column" }}>
        {activeTab === "manual" ? (
          <ManualEditTab node={node} onChange={handleChange} />
        ) : (
          <InterviewTab node={node} apiKey={apiKey} modelId={modelId} onChange={handleChange} addToast={addToast} />
        )}
      </div>
    </div>
  );
}

// ─── Summary Bar ────────────────────────────────────────────────────────────

function SummaryBar({ nodes }) {
  const processNodes = nodes.filter(n => n.data?.nodeType === "process");
  const queueNodes = nodes.filter(n => n.data?.nodeType === "queue");
  const changeoverNodes = nodes.filter(n => n.data?.nodeType === "changeover");

  let vaTime = 0, nvaTime = 0, vaUnverified = false, nvaUnverified = false;

  processNodes.forEach(n => {
    const f = n.data.fields["Cycle Time"];
    if (f?.value) {
      const mins = parseDurationToMinutes(f.value);
      if (mins !== null) {
        vaTime += mins;
        if (!f.verified) vaUnverified = true;
      }
    }
  });

  queueNodes.forEach(n => {
    const f = n.data.fields["Wait Time"];
    if (f?.value) {
      const mins = parseDurationToMinutes(f.value);
      if (mins !== null) {
        nvaTime += mins;
        if (!f.verified) nvaUnverified = true;
      }
    }
  });

  changeoverNodes.forEach(n => {
    const f = n.data.fields["Estimated Duration"];
    if (f?.value) {
      const mins = parseDurationToMinutes(f.value);
      if (mins !== null) {
        nvaTime += mins;
        if (!f.verified) nvaUnverified = true;
      }
    }
  });

  const total = vaTime + nvaTime;
  const efficiency = total > 0 ? Math.round((vaTime / total) * 100) : null;

  function fmt(mins) {
    if (mins === 0) return "0 min";
    if (mins >= 60) return `${(mins / 60).toFixed(1)} hr`;
    return `${mins.toFixed(1)} min`;
  }

  return (
    <div style={{ display: "flex", alignItems: "center", gap: 24, padding: "0 20px", height: 40, background: "#F8FAFC", borderBottom: "1px solid #E5E7EB", flexShrink: 0 }}>
      <SummaryItem label="Value-Adding Time" value={vaTime > 0 ? fmt(vaTime) + (vaUnverified ? " *" : "") : "—"} color="#22C55E" tooltip={vaUnverified ? "Some values are unverified" : ""} />
      <div style={{ width: 1, height: 20, background: "#E5E7EB" }} />
      <SummaryItem label="Non-Value-Adding Time" value={nvaTime > 0 ? fmt(nvaTime) + (nvaUnverified ? " *" : "") : "—"} color="#EF4444" tooltip={nvaUnverified ? "Some values are unverified" : ""} />
      <div style={{ width: 1, height: 20, background: "#E5E7EB" }} />
      <SummaryItem label="Process Efficiency" value={efficiency !== null ? `${efficiency}%` : "—"} color="#3B82F6" />
      {vaUnverified || nvaUnverified ? (
        <span style={{ fontSize: 11, color: "#9CA3AF", marginLeft: -8 }}>* unverified values</span>
      ) : null}
    </div>
  );
}

function SummaryItem({ label, value, color, tooltip }) {
  return (
    <div style={{ display: "flex", alignItems: "center", gap: 8 }} title={tooltip}>
      <span style={{ fontSize: 11, color: "#9CA3AF", textTransform: "uppercase", fontWeight: 600, letterSpacing: "0.05em" }}>{label}</span>
      <span style={{ fontSize: 14, fontWeight: 700, color }}>{value}</span>
    </div>
  );
}

// ─── Discovery Chat Component ────────────────────────────────────────────────

function DiscoveryChat({ apiKey, modelId, onComplete, onSkip, onOpenSettings, addToast }) {
  const [messages, setMessages] = useState([]);
  const [input, setInput] = useState("");
  const [loading, setLoading] = useState(false);
  const [readyToGenerate, setReadyToGenerate] = useState(false);
  const [generating, setGenerating] = useState(false);
  const [lastJson, setLastJson] = useState(null);
  const chatEndRef = useRef(null);

  // Auto-start with opening message
  useEffect(() => {
    if (messages.length === 0) {
      setMessages([{
        role: "assistant",
        content: "Hi! I'm here to help you map out your production process. Let's start with the basics \u2014 what does your business make, and which product line or process would you like to map?"
      }]);
    }
  }, []);

  // Auto-scroll on new messages
  useEffect(() => {
    chatEndRef.current?.scrollIntoView({ behavior: "smooth" });
  }, [messages, loading]);

  async function sendMessage() {
    const text = input.trim();
    if (!text || loading) return;
    if (!apiKey) {
      addToast({ message: "Please set your API key in Settings first.", id: makeNodeId() });
      return;
    }

    const userMsg = { role: "user", content: text };
    const newHistory = [...messages, userMsg];
    setMessages(newHistory);
    setInput("");
    setLoading(true);

    try {
      const systemPrompt = buildDiscoverySystemPrompt();
      const response = await fetch("https://openrouter.ai/api/v1/chat/completions", {
        method: "POST",
        headers: {
          "Authorization": `Bearer ${apiKey}`,
          "HTTP-Referer": "process-map-builder",
          "Content-Type": "application/json"
        },
        body: JSON.stringify({
          model: modelId || "nvidia/nemotron-3-nano-30b-a3b:free",
          messages: [{ role: "system", content: systemPrompt }, ...newHistory]
        })
      });

      if (!response.ok) {
        const errText = await response.text();
        throw new Error(`API error ${response.status}: ${errText}`);
      }

      const data = await response.json();
      const assistantContent = data.choices?.[0]?.message?.content || "";
      const assistantMsg = { role: "assistant", content: assistantContent };
      setMessages([...newHistory, assistantMsg]);

      // Extract running JSON
      const parsed = extractJSON(assistantContent);
      if (parsed) {
        setLastJson(parsed);
        if (parsed.ready_to_generate) {
          setReadyToGenerate(true);
        }
      }
    } catch (err) {
      setMessages([...newHistory, { role: "assistant", content: `Sorry, I encountered an error: ${err.message}` }]);
    } finally {
      setLoading(false);
    }
  }

  function handleGenerate() {
    if (!lastJson) {
      addToast({ message: "I don't have enough information yet. Keep chatting or skip to a blank canvas.", id: makeNodeId() });
      return;
    }
    setGenerating(true);
    // Small delay for visual feedback
    setTimeout(() => {
      const result = generateDiagramFromDiscovery(lastJson);
      if (!result || result.nodes.length === 0) {
        setGenerating(false);
        addToast({ message: "I had trouble building the diagram. Try adding more detail, or skip to a blank canvas.", id: makeNodeId() });
        return;
      }
      onComplete(result.nodes, result.edges);
    }, 600);
  }

  function handleKeyDown(e) {
    if (e.key === "Enter" && !e.shiftKey) {
      e.preventDefault();
      sendMessage();
    }
  }

  // Strip JSON blocks from display
  function displayContent(content) {
    const stripped = content.replace(/```json[\s\S]*?```/g, "").replace(/\{[\s\S]*?"process_steps"[\s\S]*?\}/g, "").trim();
    return stripped || content;
  }

  return (
    <div className="discovery-container">
      {/* Header */}
      <div style={{ height: 56, background: "#1E293B", display: "flex", alignItems: "center", padding: "0 20px", gap: 16, flexShrink: 0, zIndex: 100 }}>
        <span style={{ fontSize: 18, fontWeight: 800, color: "white", letterSpacing: "-0.02em" }}>
          Process Map Builder
        </span>
        <span style={{ fontSize: 13, color: "#94A3B8", fontWeight: 500 }}>Discovery Interview</span>
        <div style={{ flex: 1 }} />
        <TopBtn onClick={onOpenSettings} label="Settings" icon="⚙️" />
        <TopBtn onClick={onSkip} label="Skip to Canvas" icon="→" />
      </div>

      {/* API key warning */}
      {!apiKey && (
        <div style={{ background: "#FEF3C7", border: "1px solid #F59E0B", padding: "14px 20px", display: "flex", alignItems: "center", justifyContent: "center", gap: 16, fontSize: 14, color: "#92400E" }}>
          <span>To use the guided interview, add your OpenRouter API key.</span>
          <button onClick={onOpenSettings} style={{ background: "#F59E0B", color: "white", border: "none", borderRadius: 8, padding: "6px 16px", fontSize: 13, fontWeight: 600, cursor: "pointer" }}>Set Up API Key</button>
          <span style={{ color: "#B45309" }}>or</span>
          <button onClick={onSkip} style={{ background: "none", border: "1px solid #D97706", borderRadius: 8, padding: "6px 16px", fontSize: 13, fontWeight: 600, cursor: "pointer", color: "#92400E" }}>Skip to blank canvas</button>
        </div>
      )}

      {/* Chat area */}
      <div className="discovery-chat-area">
        <div className="discovery-chat-inner">
          {messages.map((msg, i) => {
            const isUser = msg.role === "user";
            return (
              <div key={i} style={{ display: "flex", justifyContent: isUser ? "flex-end" : "flex-start" }}>
                <div
                  className={isUser ? "chat-bubble-user" : "chat-bubble-assistant"}
                  style={{ maxWidth: "85%", padding: "12px 16px", fontSize: 14, lineHeight: 1.6, whiteSpace: "pre-wrap" }}
                >
                  {isUser ? msg.content : displayContent(msg.content)}
                </div>
              </div>
            );
          })}
          {loading && (
            <div style={{ display: "flex", justifyContent: "flex-start" }}>
              <div className="chat-bubble-assistant" style={{ padding: "12px 16px" }}>
                <span className="spinning" style={{ display: "inline-block", fontSize: 16 }}>⟳</span>
              </div>
            </div>
          )}
          {readyToGenerate && !loading && !generating && (
            <div style={{ display: "flex", justifyContent: "center", padding: "8px 0" }}>
              <button className="discovery-generate-btn" onClick={handleGenerate}>
                🗺️ Generate Map
              </button>
            </div>
          )}
          <div ref={chatEndRef} />
        </div>
      </div>

      {/* Input bar */}
      <div className="discovery-input-bar">
        <div className="discovery-input-inner">
          <input
            type="text"
            value={input}
            onChange={e => setInput(e.target.value)}
            onKeyDown={handleKeyDown}
            placeholder={apiKey ? "Describe your process..." : "Set up API key first"}
            disabled={!apiKey || loading}
            style={{
              flex: 1, padding: "12px 16px", borderRadius: 12, border: "1.5px solid #E5E7EB",
              fontSize: 14, outline: "none", transition: "border-color 0.15s",
              background: !apiKey ? "#F9FAFB" : "white"
            }}
            onFocus={e => e.target.style.borderColor = "#3B82F6"}
            onBlur={e => e.target.style.borderColor = "#E5E7EB"}
          />
          <button
            onClick={sendMessage}
            disabled={!apiKey || loading || !input.trim()}
            style={{
              background: !apiKey || loading || !input.trim() ? "#D1D5DB" : "#3B82F6",
              color: "white", border: "none", borderRadius: 12, padding: "12px 20px",
              fontSize: 14, fontWeight: 600, cursor: !apiKey || loading ? "not-allowed" : "pointer",
              transition: "background 0.15s"
            }}
          >
            Send
          </button>
        </div>
      </div>

      {/* Generating overlay */}
      {generating && (
        <div className="generating-overlay">
          <span className="spinning" style={{ fontSize: 40 }}>🗺️</span>
          <div style={{ fontSize: 18, fontWeight: 700, color: "#1E293B" }}>Building your process map...</div>
          <div style={{ fontSize: 14, color: "#6B7280" }}>This will just take a moment</div>
        </div>
      )}
    </div>
  );
}

// ─── Main App (inner, needs useReactFlow) ───────────────────────────────────

function AppInner() {
  const [nodes, setNodes, onNodesChange] = useNodesState([]);
  const [edges, setEdges, onEdgesChange] = useEdgesState([]);
  const [selectedNodeId, setSelectedNodeId] = useState(null);
  const [apiKey, setApiKey] = useState(window.OPENROUTER_API_KEY || "");
  const [modelId, setModelId] = useState("nvidia/nemotron-3-nano-30b-a3b:free");
  const [appMode, setAppMode] = useState("discovery"); // "discovery" | "canvas"
  const [showSettings, setShowSettings] = useState(false);
  const [toasts, setToasts] = useState([]);
  const [dismissedQueuePrompts, setDismissedQueuePrompts] = useState(new Set());
  const [dismissedChangeoverPrompts, setDismissedChangeoverPrompts] = useState(new Set());

  const reactFlow = useReactFlow();
  const changeoverTimers = useRef({});

  // ── Selection tracking ──────────────────────────────────────────────────
  const onNodeClick = useCallback((event, node) => {
    setSelectedNodeId(node.id);
  }, []);

  const onPaneClick = useCallback(() => {
    setSelectedNodeId(null);
  }, []);

  const selectedNode = useMemo(() => nodes.find(n => n.id === selectedNodeId) || null, [nodes, selectedNodeId]);

  // ── Bottleneck detection ────────────────────────────────────────────────
  const bottleneckIds = useMemo(() => {
    const processNodes = nodes.filter(n => n.data?.nodeType === "process");
    const cycleTimes = processNodes.map(n => parseDurationToMinutes(n.data.fields["Cycle Time"]?.value)).filter(v => v !== null);
    if (cycleTimes.length < 2) return new Set();
    const mean = cycleTimes.reduce((a, b) => a + b, 0) / cycleTimes.length;
    const threshold = mean * 1.2;
    const ids = new Set();
    processNodes.forEach(n => {
      const ct = parseDurationToMinutes(n.data.fields["Cycle Time"]?.value);
      if (ct !== null && ct > threshold) ids.add(n.id);
    });
    return ids;
  }, [nodes]);

  // ── Toast helpers ───────────────────────────────────────────────────────
  function addToast(toast) {
    const id = toast.id || makeNodeId();
    setToasts(prev => [...prev, { ...toast, id }]);
    if (!toast.actions) {
      setTimeout(() => dismissToast(id), 5000);
    }
  }

  function dismissToast(id) {
    setToasts(prev => prev.filter(t => t.id !== id));
  }

  // ── Add node ────────────────────────────────────────────────────────────
  function addNode(nodeType) {
    const viewport = reactFlow.getViewport();
    const centerX = (window.innerWidth / 2 - viewport.x) / viewport.zoom;
    const centerY = (window.innerHeight / 2 - viewport.y) / viewport.zoom;
    const jitter = () => (Math.random() - 0.5) * 100;
    const newNode = createNode(nodeType, { x: centerX + jitter(), y: centerY + jitter() });
    setNodes(prev => [...prev, newNode]);
  }

  // ── Update node ─────────────────────────────────────────────────────────
  function updateNode(nodeId, patch) {
    setNodes(prev => prev.map(n => {
      if (n.id !== nodeId) return n;
      const newData = { ...n.data, ...patch };
      // If fields updated, merge
      if (patch.fields) newData.fields = { ...n.data.fields, ...patch.fields };
      return { ...n, data: newData };
    }));
  }

  // ── Changeover prompt on label change ───────────────────────────────────
  useEffect(() => {
    nodes.forEach(n => {
      if (n.data?.nodeType !== "process") return;
      const cfg = NODE_TYPES_CONFIG.process;
      const defaultPrefix = cfg.label;
      if (!n.data.label.startsWith(defaultPrefix) && !dismissedChangeoverPrompts.has(n.id)) {
        if (changeoverTimers.current[n.id]) return;
        changeoverTimers.current[n.id] = setTimeout(() => {
          delete changeoverTimers.current[n.id];
          if (dismissedChangeoverPrompts.has(n.id)) return;
          const toastId = "co_" + n.id;
          setToasts(prev => {
            if (prev.some(t => t.id === toastId)) return prev;
            return [...prev, {
              id: toastId,
              message: `Does "${n.data.label}" ever switch between different products or variants? If so, consider adding a Changeover node.`,
              actions: [
                {
                  label: "Add Changeover",
                  onClick: () => {
                    const pos = { x: n.position.x + 200, y: n.position.y + 100 };
                    const coNode = createNode("changeover", pos);
                    setNodes(prev => [...prev, coNode]);
                    setDismissedChangeoverPrompts(prev => new Set([...prev, n.id]));
                  }
                },
                {
                  label: "Dismiss",
                  onClick: () => setDismissedChangeoverPrompts(prev => new Set([...prev, n.id]))
                }
              ]
            }];
          });
        }, 3000);
      }
    });
  }, [nodes.map(n => n.data?.label).join(",")]);

  // ── Connect handler ─────────────────────────────────────────────────────
  const onConnect = useCallback((params) => {
    const newEdge = {
      ...params,
      id: makeEdgeId(),
      markerEnd: { type: MarkerType.ArrowClosed },
      style: { strokeWidth: 2 }
    };
    setEdges(prev => addEdge(newEdge, prev));

    // Check if connecting two process nodes
    const sourceNode = nodes.find(n => n.id === params.source);
    const targetNode = nodes.find(n => n.id === params.target);
    if (sourceNode?.data?.nodeType === "process" && targetNode?.data?.nodeType === "process") {
      const promptKey = `${params.source}-${params.target}`;
      if (dismissedQueuePrompts.has(promptKey)) return;

      // Check if there's already a queue between them
      const hasQueue = nodes.some(n => {
        if (n.data?.nodeType !== "queue") return false;
        const inEdge = edges.some(e => e.target === n.id && e.source === params.source);
        const outEdge = edges.some(e => e.source === n.id && e.target === params.target);
        return inEdge && outEdge;
      });

      if (!hasQueue) {
        const toastId = "queue_" + promptKey;
        setTimeout(() => {
          setToasts(prev => {
            if (prev.some(t => t.id === toastId)) return prev;
            return [...prev, {
              id: toastId,
              message: `Is there any waiting or pile-up of material between "${sourceNode.data.label}" and "${targetNode.data.label}"? Consider adding a Wait/Queue node.`,
              actions: [
                {
                  label: "Add Queue Node",
                  onClick: () => {
                    const midX = (sourceNode.position.x + targetNode.position.x) / 2;
                    const midY = (sourceNode.position.y + targetNode.position.y) / 2;
                    const queueNode = createNode("queue", { x: midX, y: midY - 80 });
                    setNodes(prev => [...prev, queueNode]);
                    setEdges(prev => {
                      const withoutDirect = prev.filter(e => !(e.source === params.source && e.target === params.target));
                      return [
                        ...withoutDirect,
                        { id: makeEdgeId(), source: params.source, target: queueNode.id, markerEnd: { type: MarkerType.ArrowClosed }, style: { strokeWidth: 2 } },
                        { id: makeEdgeId(), source: queueNode.id, target: params.target, markerEnd: { type: MarkerType.ArrowClosed }, style: { strokeWidth: 2 } }
                      ];
                    });
                    setDismissedQueuePrompts(prev => new Set([...prev, promptKey]));
                  }
                },
                {
                  label: "Dismiss",
                  onClick: () => setDismissedQueuePrompts(prev => new Set([...prev, promptKey]))
                }
              ]
            }];
          });
        }, 300);
      }
    }
  }, [nodes, edges, dismissedQueuePrompts]);

  // ── Export / Import ─────────────────────────────────────────────────────
  function exportJSON() {
    const data = { version: "1.0", exportedAt: new Date().toISOString(), nodes, edges };
    const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" });
    const url = URL.createObjectURL(blob);
    const a = document.createElement("a");
    a.href = url;
    a.download = `process-map-${Date.now()}.json`;
    a.click();
    URL.revokeObjectURL(url);
    addToast({ message: "Process map exported successfully.", id: makeNodeId() });
  }

  function importJSON() {
    const input = document.createElement("input");
    input.type = "file";
    input.accept = ".json";
    input.onchange = e => {
      const file = e.target.files[0];
      if (!file) return;
      const reader = new FileReader();
      reader.onload = ev => {
        try {
          const data = JSON.parse(ev.target.result);
          if (!Array.isArray(data.nodes) || !Array.isArray(data.edges)) {
            throw new Error("Invalid format: missing nodes or edges arrays.");
          }
          setNodes(data.nodes);
          setEdges(data.edges);
          setSelectedNodeId(null);
          setAppMode("canvas");
          addToast({ message: `Imported ${data.nodes.length} nodes and ${data.edges.length} edges.`, id: makeNodeId() });
        } catch (err) {
          addToast({ message: `Import failed: ${err.message}`, id: makeNodeId() });
        }
      };
      reader.readAsText(file);
    };
    input.click();
  }

  function fitView() {
    reactFlow.fitView({ padding: 0.1, duration: 500 });
  }

  // ── Discovery ─────────────────────────────────────────────────────────
  function handleDiscoveryComplete(generatedNodes, generatedEdges) {
    setNodes(generatedNodes);
    setEdges(generatedEdges);
    setSelectedNodeId(null);
    setAppMode("canvas");
    setTimeout(() => {
      reactFlow.fitView({ padding: 0.2, duration: 500 });
    }, 100);
    addToast({
      message: `Process map created with ${generatedNodes.length} nodes. Click any node to review and add detail.`,
      id: makeNodeId()
    });
  }

  function handleNewMap() {
    if (nodes.length > 0) {
      const toastId = makeNodeId();
      addToast({
        id: toastId,
        message: "Start a new discovery? This will replace your current map.",
        actions: [
          { label: "Start New", onClick: () => { setNodes([]); setEdges([]); setSelectedNodeId(null); setAppMode("discovery"); } },
          { label: "Cancel", onClick: () => {} }
        ]
      });
      return;
    }
    setAppMode("discovery");
  }

  function handleSettingsSave(newKey, newModel) {
    setApiKey(newKey);
    if (newModel && newModel !== "__custom__") setModelId(newModel);
  }

  // ── Render ──────────────────────────────────────────────────────────────
  return (
    <BottleneckContext.Provider value={bottleneckIds}>
      {appMode === "discovery" ? (
        <DiscoveryChat
          apiKey={apiKey}
          modelId={modelId}
          onComplete={handleDiscoveryComplete}
          onSkip={() => setAppMode("canvas")}
          onOpenSettings={() => setShowSettings(true)}
          addToast={addToast}
        />
      ) : (
      <div style={{ display: "flex", flexDirection: "column", height: "100vh", width: "100vw" }}>
        {/* Top Bar */}
        <div style={{ height: 56, background: "#1E293B", display: "flex", alignItems: "center", padding: "0 20px", gap: 16, flexShrink: 0, zIndex: 100 }}>
          <span style={{ fontSize: 18, fontWeight: 800, color: "white", letterSpacing: "-0.02em", marginRight: 8 }}>
            Process Map Builder
          </span>
          <div style={{ flex: 1 }} />
          <TopBtn onClick={handleNewMap} label="New Map" icon="✨" />
          <TopBtn onClick={exportJSON} label="Export JSON" icon="⬇" />
          <TopBtn onClick={importJSON} label="Import JSON" icon="⬆" />
          <TopBtn onClick={() => setShowSettings(true)} label="Settings" icon="⚙️" />
          <TopBtn onClick={fitView} label="Fit to Screen" icon="⤢" />
        </div>

        {/* Summary Bar */}
        <SummaryBar nodes={nodes} />

        {/* Main Content */}
        <div style={{ flex: 1, display: "flex", overflow: "hidden" }}>
          {/* Left Sidebar */}
          <div style={{ width: 200, background: "#F8FAFC", borderRight: "1px solid #E5E7EB", padding: "16px 12px", flexShrink: 0, display: "flex", flexDirection: "column", gap: 8 }}>
            <div style={{ fontSize: 11, fontWeight: 700, color: "#9CA3AF", textTransform: "uppercase", letterSpacing: "0.08em", marginBottom: 4 }}>Add Node</div>
            {Object.entries(NODE_TYPES_CONFIG).map(([type, cfg]) => (
              <button
                key={type}
                onClick={() => addNode(type)}
                style={{
                  display: "flex", alignItems: "center", gap: 10, padding: "10px 12px",
                  background: cfg.bg, border: `1.5px solid ${cfg.border}`, borderRadius: 10,
                  cursor: "pointer", fontSize: 13, fontWeight: 600, color: "#374151",
                  transition: "transform 0.1s, box-shadow 0.1s",
                  boxShadow: "0 1px 3px rgba(0,0,0,0.06)"
                }}
                onMouseOver={e => { e.currentTarget.style.transform = "translateY(-1px)"; e.currentTarget.style.boxShadow = "0 3px 8px rgba(0,0,0,0.12)"; }}
                onMouseOut={e => { e.currentTarget.style.transform = ""; e.currentTarget.style.boxShadow = "0 1px 3px rgba(0,0,0,0.06)"; }}
              >
                <span style={{ fontSize: 18 }}>{cfg.icon}</span>
                <span>{cfg.label}</span>
              </button>
            ))}

            <div style={{ marginTop: "auto", fontSize: 11, color: "#CBD5E1", lineHeight: 1.5 }}>
              <div style={{ fontWeight: 600, color: "#94A3B8", marginBottom: 4 }}>Legend</div>
              <div style={{ display: "flex", alignItems: "center", gap: 6 }}><span style={{ width: 8, height: 8, borderRadius: "50%", background: "#22C55E", display: "inline-block" }} /> Complete</div>
              <div style={{ display: "flex", alignItems: "center", gap: 6 }}><span style={{ width: 8, height: 8, borderRadius: "50%", background: "#F59E0B", display: "inline-block" }} /> Partial</div>
              <div style={{ display: "flex", alignItems: "center", gap: 6 }}><span style={{ width: 8, height: 8, borderRadius: "50%", background: "#EF4444", display: "inline-block" }} /> Empty</div>
            </div>
          </div>

          {/* Canvas */}
          <div style={{ flex: 1, position: "relative" }}>
            <ReactFlow
              nodes={nodes}
              edges={edges}
              onNodesChange={onNodesChange}
              onEdgesChange={onEdgesChange}
              onConnect={onConnect}
              onNodeClick={onNodeClick}
              onPaneClick={onPaneClick}
              nodeTypes={nodeTypes}
              fitView
              proOptions={{ hideAttribution: true }}
              defaultEdgeOptions={{ markerEnd: { type: MarkerType.ArrowClosed }, style: { strokeWidth: 2 } }}
            >
              <Background color="#E2E8F0" gap={20} />
              <Controls />
              <MiniMap nodeColor={n => NODE_TYPES_CONFIG[n.data?.nodeType]?.bg || "#E5E7EB"} />
            </ReactFlow>
            {nodes.length === 0 && (
              <div style={{ position: "absolute", inset: 0, display: "flex", alignItems: "center", justifyContent: "center", pointerEvents: "none" }}>
                <div style={{ textAlign: "center", color: "#94A3B8" }}>
                  <div style={{ fontSize: 48, marginBottom: 16 }}>🗺️</div>
                  <div style={{ fontSize: 18, fontWeight: 700, marginBottom: 8 }}>Start building your process map</div>
                  <div style={{ fontSize: 14 }}>Click a node type on the left to add it to the canvas</div>
                </div>
              </div>
            )}
          </div>

          {/* Right Panel */}
          {selectedNode && (
            <NodeDetailPanel
              node={selectedNode}
              apiKey={apiKey}
              modelId={modelId}
              onUpdate={updateNode}
              addToast={addToast}
              onClose={() => setSelectedNodeId(null)}
            />
          )}
        </div>
      </div>
      )}

      {/* Toasts and Settings render in BOTH modes */}
      <ToastContainer toasts={toasts} onDismiss={dismissToast} />
      {showSettings && (
        <SettingsModal
          apiKey={apiKey}
          modelId={modelId}
          onSave={handleSettingsSave}
          onClose={() => setShowSettings(false)}
        />
      )}
    </BottleneckContext.Provider>
  );
}

function TopBtn({ onClick, label, icon }) {
  return (
    <button
      onClick={onClick}
      style={{ display: "flex", alignItems: "center", gap: 6, padding: "6px 14px", background: "rgba(255,255,255,0.1)", border: "1px solid rgba(255,255,255,0.2)", borderRadius: 8, color: "white", fontSize: 13, cursor: "pointer", fontWeight: 500, transition: "background 0.15s" }}
      onMouseOver={e => e.currentTarget.style.background = "rgba(255,255,255,0.2)"}
      onMouseOut={e => e.currentTarget.style.background = "rgba(255,255,255,0.1)"}
    >
      <span>{icon}</span>
      <span>{label}</span>
    </button>
  );
}

function App() {
  return (
    <ReactFlowProvider>
      <AppInner />
    </ReactFlowProvider>
  );
}

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);
