// MDM SORP — Relationship Map screen
// ER-like diagram with FOUR semantic view modes:
//   Business    — бизнес-объекты (Company, Contact, Deal, Invoice, Payment, Task, Document, User)
//   Technical   — реальные таблицы из БД
//   Integration — откуда данные приходят (AmoCRM/Asana/Sheets/Bank/Email → tables)
//   AI          — где работают AI-агенты (extraction/matching/dedup/scoring/...)
//
// SVG-based, pan/zoom, node selection, side panel with details.

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

  // ─────────────────────────────────────────────────────────────
  // Modes — node lists + edges + colors per mode
  // ─────────────────────────────────────────────────────────────

  // Compact node spec: id, x, y, color, group, label, sub, fields?
  const M_BUSINESS = {
    label: "Business view",
    desc: "Бизнес-объекты — то, чем оперирует бизнес. Без технических деталей.",
    nodeColor: "#2C5C8C",
    nodes: [
      { id: "company",  x: 80,  y: 80,  label: "Company",  sub: "Контрагенты",  icon: "parties" },
      { id: "contact",  x: 80,  y: 220, label: "Contact",  sub: "Физ. лица",    icon: "parties" },
      { id: "user",     x: 80,  y: 360, label: "User",     sub: "Наши сотрудники", icon: "parties" },
      { id: "deal",     x: 360, y: 80,  label: "Deal",     sub: "Сделки SORP-XXXX", icon: "deals" },
      { id: "task",     x: 360, y: 220, label: "Task",     sub: "Задачи проджекта", icon: "queue" },
      { id: "invoice",  x: 640, y: 80,  label: "Invoice",  sub: "Счета", icon: "docs" },
      { id: "payment",  x: 640, y: 220, label: "Payment",  sub: "Платежи", icon: "bar" },
      { id: "document", x: 640, y: 360, label: "Document", sub: "Файлы и сканы", icon: "docs" },
    ],
    edges: [
      { from: "company",  to: "deal",     label: "owns" },
      { from: "contact",  to: "company",  label: "works_at" },
      { from: "user",     to: "deal",     label: "manages" },
      { from: "user",     to: "task",     label: "assignee" },
      { from: "deal",     to: "task",     label: "spawns" },
      { from: "deal",     to: "invoice",  label: "produces" },
      { from: "invoice",  to: "payment",  label: "paid_by" },
      { from: "deal",     to: "document", label: "has" },
    ],
    legend: [
      { color: "#2C5C8C", label: "Бизнес-сущности" },
      { color: "#7A7468", label: "Связи (semantic)" },
    ],
  };

  const M_TECHNICAL = {
    label: "Technical view",
    desc: "Реальные таблицы Postgres. Поля, типы, FK — то, что видит разработчик.",
    nodeColor: "#1E2329",
    nodes: [
      { id: "counterparties", x: 80,  y: 60,  label: "counterparties", sub: "table", fields: [
        { k: "id", t: "uuid", pk: true },
        { k: "type", t: "enum" },
        { k: "short_name", t: "text" },
        { k: "inn", t: "text" },
      ] },
      { id: "deals", x: 360, y: 60, label: "deals", sub: "table", fields: [
        { k: "id", t: "uuid", pk: true },
        { k: "sorp_number", t: "text" },
        { k: "customer_id", t: "uuid → cp", fk: true },
        { k: "supplier_id", t: "uuid → cp", fk: true },
        { k: "current_stage", t: "enum" },
        { k: "margin_amount", t: "numeric (gen)" },
      ] },
      { id: "deal_items", x: 660, y: 60, label: "deal_items", sub: "table", fields: [
        { k: "id", t: "uuid", pk: true },
        { k: "deal_id", t: "uuid → deals", fk: true },
        { k: "name", t: "text" },
        { k: "quantity", t: "numeric" },
      ] },
      { id: "contracts", x: 360, y: 280, label: "contracts", sub: "table", fields: [
        { k: "id", t: "uuid", pk: true },
        { k: "deal_id", t: "uuid → deals", fk: true },
        { k: "party_type", t: "enum" },
        { k: "amount_rub", t: "numeric" },
      ] },
      { id: "payments", x: 660, y: 280, label: "payments", sub: "table", fields: [
        { k: "id", t: "uuid", pk: true },
        { k: "contract_id", t: "uuid → ctr", fk: true },
        { k: "amount", t: "numeric" },
        { k: "direction", t: "enum" },
      ] },
      { id: "transitions_log", x: 80, y: 280, label: "transitions_log", sub: "append-only", fields: [
        { k: "id", t: "uuid", pk: true },
        { k: "deal_id", t: "uuid → deals", fk: true },
        { k: "stage_from", t: "enum" },
        { k: "stage_to", t: "enum" },
      ] },
      { id: "employees", x: 80, y: 500, label: "employees", sub: "table", fields: [
        { k: "id", t: "uuid", pk: true },
        { k: "full_name", t: "text" },
        { k: "role", t: "enum" },
      ] },
      { id: "documents", x: 660, y: 500, label: "documents", sub: "table", fields: [
        { k: "id", t: "uuid", pk: true },
        { k: "deal_id", t: "uuid → deals", fk: true },
        { k: "doc_type", t: "enum" },
        { k: "url", t: "text" },
      ] },
      { id: "shipments", x: 360, y: 500, label: "shipments", sub: "table", fields: [
        { k: "id", t: "uuid", pk: true },
        { k: "contract_id", t: "uuid → ctr", fk: true },
        { k: "status", t: "enum" },
      ] },
    ],
    edges: [
      { from: "deals", to: "counterparties", label: "customer_id" },
      { from: "deals", to: "counterparties", label: "supplier_id", curveOffset: 30 },
      { from: "deal_items", to: "deals", label: "FK" },
      { from: "contracts", to: "deals", label: "FK" },
      { from: "payments", to: "contracts", label: "FK" },
      { from: "transitions_log", to: "deals", label: "FK" },
      { from: "documents", to: "deals", label: "FK" },
      { from: "shipments", to: "contracts", label: "FK" },
      { from: "deals", to: "employees", label: "pm_id" },
    ],
    legend: [
      { color: "#1E2329", label: "table" },
      { color: "#D63D4A", label: "PK column" },
      { color: "#2C5C8C", label: "FK column" },
    ],
  };

  const M_INTEGRATION = {
    label: "Integration view",
    desc: "Откуда данные приходят. Стрелка = поток данных от источника к таблице.",
    nodeColor: "#1E5E40",
    nodes: [
      // Sources
      { id: "amocrm",  x: 60,  y: 80,  label: "AmoCRM",         sub: "source · OAuth", icon: "plug", kind: "source", srcColor: "#FF8A0B" },
      { id: "asana",   x: 60,  y: 200, label: "Asana",          sub: "source · PAT",   icon: "plug", kind: "source", srcColor: "#F06A6A" },
      { id: "sheets",  x: 60,  y: 320, label: "Google Sheets",  sub: "source · OAuth", icon: "plug", kind: "source", srcColor: "#0F9D58" },
      { id: "bank",    x: 60,  y: 440, label: "Bank statements",sub: "source · CSV",   icon: "plug", kind: "source", srcColor: "#2C5C8C" },
      { id: "email",   x: 60,  y: 560, label: "Email",          sub: "source · IMAP",  icon: "plug", kind: "source", srcColor: "#7A7468" },

      // Tables
      { id: "deals",      x: 440, y: 60,  label: "deals",           sub: "table" },
      { id: "tasks",      x: 440, y: 200, label: "tasks",           sub: "table" },
      { id: "fin_staging",x: 440, y: 320, label: "fin_staging",     sub: "staging table" },
      { id: "bank_txn",   x: 440, y: 440, label: "bank_transactions", sub: "table" },
      { id: "documents",  x: 440, y: 560, label: "documents",       sub: "table" },
    ],
    edges: [
      { from: "amocrm", to: "deals",       label: "leads → deals", kind: "integration" },
      { from: "asana",  to: "tasks",       label: "tasks → tasks", kind: "integration" },
      { from: "asana",  to: "deals",       label: "GID → deal.asana_task_gid", kind: "integration", curveOffset: -40 },
      { from: "sheets", to: "fin_staging", label: "sheet → staging", kind: "integration" },
      { from: "bank",   to: "bank_txn",    label: "csv → txn", kind: "integration" },
      { from: "email",  to: "documents",   label: "attachment → doc", kind: "integration" },
    ],
    legend: [
      { color: "#FF8A0B", label: "AmoCRM source" },
      { color: "#F06A6A", label: "Asana source" },
      { color: "#0F9D58", label: "Sheets source" },
      { color: "#2C5C8C", label: "Bank source" },
      { color: "#7A7468", label: "Email source" },
      { color: "#1E5E40", label: "Destination table" },
    ],
  };

  const M_AI = {
    label: "AI view",
    desc: "Где работают AI-агенты. Стрелка = вход и выход агента.",
    nodeColor: "#4A2BA8",
    nodes: [
      // Inputs
      { id: "raw_text",     x: 60,  y: 80,  label: "raw text",        sub: "Asana comments, emails", icon: "list", kind: "data" },
      { id: "raw_pdf",      x: 60,  y: 220, label: "PDF / scan",      sub: "spec, invoice, КП",     icon: "file", kind: "data" },
      { id: "csv_data",     x: 60,  y: 360, label: "CSV / sheet",     sub: "bank, finance",         icon: "table", kind: "data" },
      { id: "duplicates",   x: 60,  y: 500, label: "duplicate candidates", sub: "fuzzy match input", icon: "merge", kind: "data" },

      // Agents
      { id: "extraction",    x: 380, y: 80,  label: "Extraction agent",   sub: "NER + regex", kind: "agent" },
      { id: "matching",      x: 380, y: 220, label: "Matching agent",     sub: "amo↔asana↔drive", kind: "agent" },
      { id: "dedup",         x: 380, y: 360, label: "Dedup agent",        sub: "fuzzy + INN + name", kind: "agent" },
      { id: "scoring",       x: 380, y: 500, label: "Scoring agent",      sub: "confidence + risk", kind: "agent" },
      { id: "reconciliation",x: 380, y: 640, label: "Reconciliation",     sub: "amount match cross-source", kind: "agent" },
      { id: "summarization", x: 700, y: 80,  label: "Summarization",      sub: "deal digest, weekly", kind: "agent" },

      // Outputs
      { id: "deals_out",     x: 700, y: 220, label: "deals",        sub: "table" },
      { id: "provenance",    x: 700, y: 360, label: "field_provenance", sub: "src + conf" },
      { id: "merged_party",  x: 700, y: 500, label: "counterparties", sub: "merged record" },
    ],
    edges: [
      { from: "raw_text", to: "extraction", label: "input", kind: "ai" },
      { from: "raw_pdf",  to: "extraction", label: "input", kind: "ai" },
      { from: "csv_data", to: "matching",   label: "input", kind: "ai" },
      { from: "duplicates", to: "dedup",    label: "input", kind: "ai" },
      { from: "extraction", to: "deals_out", label: "fields", kind: "ai" },
      { from: "extraction", to: "provenance", label: "conf+raw", kind: "ai" },
      { from: "matching", to: "provenance", label: "match score", kind: "ai" },
      { from: "dedup",    to: "merged_party", label: "merged uuid", kind: "ai" },
      { from: "scoring",  to: "provenance", label: "writes conf",  kind: "ai" },
      { from: "reconciliation", to: "deals_out", label: "amount_rub", kind: "ai", curveOffset: -50 },
      { from: "extraction", to: "summarization", label: "digest input", kind: "ai" },
    ],
    legend: [
      { color: "#4A2BA8", label: "AI agent" },
      { color: "#7A7468", label: "Input data" },
      { color: "#1E5E40", label: "Destination" },
      { color: "#6E45E2", label: "Data flow", dash: true },
    ],
  };

  const MODES = { business: M_BUSINESS, technical: M_TECHNICAL, integration: M_INTEGRATION, ai: M_AI };

  // ─────────────────────────────────────────────────────────────
  // Node sizing — width fixed, height depends on fields count
  // ─────────────────────────────────────────────────────────────
  const NODE_W = 220;
  const HEADER_H = 36;
  const FIELD_H = 22;

  function nodeHeight(node) {
    if (node.fields && node.fields.length) return HEADER_H + node.fields.length * FIELD_H + 8;
    return HEADER_H + 32; // sub line
  }

  // Compute attachment point on the side of a node closest to target center
  function attachPoint(node, targetCx, targetCy) {
    const w = NODE_W;
    const h = nodeHeight(node);
    const cx = node.x + w / 2;
    const cy = node.y + h / 2;
    const dx = targetCx - cx;
    const dy = targetCy - cy;
    // Decide if attach is on left/right or top/bottom side
    if (Math.abs(dx) / w > Math.abs(dy) / h) {
      // attach on left or right
      return { x: dx > 0 ? node.x + w : node.x, y: cy };
    } else {
      return { x: cx, y: dy > 0 ? node.y + h : node.y };
    }
  }

  function curvePath(from, to, offset = 0) {
    const dx = to.x - from.x;
    const dy = to.y - from.y;
    const cx1 = from.x + dx * 0.5;
    const cy1 = from.y + dy * 0.1 + offset;
    const cx2 = from.x + dx * 0.5;
    const cy2 = to.y - dy * 0.1 + offset;
    return `M ${from.x} ${from.y} C ${cx1} ${cy1}, ${cx2} ${cy2}, ${to.x} ${to.y}`;
  }

  // ─────────────────────────────────────────────────────────────
  // Main screen
  // ─────────────────────────────────────────────────────────────
  function RelationshipMapScreen() {
    const [mode, setMode] = useState("business");
    const [selected, setSelected] = useState(null);
    const [transform, setTransform] = useState({ x: 0, y: 0, k: 1 });
    const vpRef = useRef(null);

    const data = MODES[mode];
    const nodes = data.nodes;
    const edges = data.edges;

    // Pan: drag background
    const dragRef = useRef(null);
    const onMouseDown = (e) => {
      if (e.target.closest(".rm-node")) return;
      dragRef.current = { startX: e.clientX, startY: e.clientY, base: { x: transform.x, y: transform.y } };
    };
    useEffect(() => {
      const move = (e) => {
        if (!dragRef.current) return;
        setTransform(t => ({ ...t, x: dragRef.current.base.x + (e.clientX - dragRef.current.startX), y: dragRef.current.base.y + (e.clientY - dragRef.current.startY) }));
      };
      const up = () => { dragRef.current = null; };
      window.addEventListener("mousemove", move);
      window.addEventListener("mouseup", up);
      return () => { window.removeEventListener("mousemove", move); window.removeEventListener("mouseup", up); };
    }, []);

    // Zoom: ctrl/cmd + wheel
    const onWheel = (e) => {
      if (!(e.ctrlKey || e.metaKey)) return;
      e.preventDefault();
      const factor = Math.exp(-e.deltaY * 0.002);
      setTransform(t => ({ ...t, k: Math.max(0.4, Math.min(2.5, t.k * factor)) }));
    };

    const zoomIn  = () => setTransform(t => ({ ...t, k: Math.min(2.5, t.k * 1.2) }));
    const zoomOut = () => setTransform(t => ({ ...t, k: Math.max(0.4, t.k / 1.2) }));
    const zoomReset = () => setTransform({ x: 0, y: 0, k: 1 });

    // Reset selection when mode changes
    useEffect(() => { setSelected(null); }, [mode]);

    const selectedNode = selected ? nodes.find(n => n.id === selected) : null;
    const incoming = selected ? edges.filter(e => e.to === selected) : [];
    const outgoing = selected ? edges.filter(e => e.from === selected) : [];

    // ─────────────────────────────────────────────────────
    // Render
    // ─────────────────────────────────────────────────────
    return (
      <div className="rm-screen">
        <div className="rm-head">
          <div>
            <h2 className="rm-title">Relationship Map</h2>
            <div className="rm-subtitle">{data.desc}</div>
          </div>
          <div className="rm-modes">
            <button className={"rm-mode" + (mode === "business" ? " active" : "")} onClick={() => setMode("business")}>
              <span className="rm-mode-dot" style={{ background: M_BUSINESS.nodeColor }}/>
              Business
            </button>
            <button className={"rm-mode" + (mode === "technical" ? " active" : "")} onClick={() => setMode("technical")}>
              <span className="rm-mode-dot" style={{ background: M_TECHNICAL.nodeColor }}/>
              Technical
            </button>
            <button className={"rm-mode" + (mode === "integration" ? " active" : "")} onClick={() => setMode("integration")}>
              <span className="rm-mode-dot" style={{ background: M_INTEGRATION.nodeColor }}/>
              Integration
            </button>
            <button className={"rm-mode" + (mode === "ai" ? " active" : "")} onClick={() => setMode("ai")}>
              <span className="rm-mode-dot" style={{ background: M_AI.nodeColor }}/>
              AI
            </button>
          </div>
        </div>

        <div className="rm-canvas-wrap" ref={vpRef} onMouseDown={onMouseDown} onWheel={onWheel} style={{ cursor: dragRef.current ? "grabbing" : "grab" }}>
          <svg className="rm-canvas" width="100%" height="100%">
            <defs>
              <marker id="rm-arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="8" markerHeight="8" orient="auto-start-reverse">
                <path d="M 0 0 L 10 5 L 0 10 z" fill="currentColor"/>
              </marker>
              <marker id="rm-arrow-blue" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="8" markerHeight="8" orient="auto-start-reverse">
                <path d="M 0 0 L 10 5 L 0 10 z" fill="#2C5C8C"/>
              </marker>
              <marker id="rm-arrow-purple" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="8" markerHeight="8" orient="auto-start-reverse">
                <path d="M 0 0 L 10 5 L 0 10 z" fill="#6E45E2"/>
              </marker>
            </defs>

            <g transform={`translate(${transform.x}, ${transform.y}) scale(${transform.k})`}>
              {/* Edges */}
              {edges.map((e, i) => {
                const from = nodes.find(n => n.id === e.from);
                const to = nodes.find(n => n.id === e.to);
                if (!from || !to) return null;
                const fromCx = from.x + NODE_W / 2;
                const fromCy = from.y + nodeHeight(from) / 2;
                const toCx = to.x + NODE_W / 2;
                const toCy = to.y + nodeHeight(to) / 2;
                const fromAtt = attachPoint(from, toCx, toCy);
                const toAtt = attachPoint(to, fromCx, fromCy);
                const dimmed = selected && e.from !== selected && e.to !== selected;
                const arrowMarker = e.kind === "integration" ? "url(#rm-arrow-blue)" : e.kind === "ai" ? "url(#rm-arrow-purple)" : "url(#rm-arrow)";
                return (
                  <g key={i} opacity={dimmed ? 0.18 : 1}>
                    <path d={curvePath(fromAtt, toAtt, e.curveOffset || 0)}
                          className={"rm-edge" + (e.kind ? " " + e.kind : "")}
                          markerEnd={arrowMarker}/>
                    {e.label && (
                      <text x={(fromAtt.x + toAtt.x) / 2} y={(fromAtt.y + toAtt.y) / 2 - 4}
                            textAnchor="middle" className="rm-edge-label">
                        {e.label}
                      </text>
                    )}
                  </g>
                );
              })}

              {/* Nodes */}
              {nodes.map(node => {
                const h = nodeHeight(node);
                const isSelected = selected === node.id;
                const dimmed = selected && !isSelected && !edges.some(e => (e.from === selected && e.to === node.id) || (e.to === selected && e.from === node.id));
                const headerColor = node.srcColor || data.nodeColor;
                return (
                  <g key={node.id} className={"rm-node" + (isSelected ? " selected" : "")}
                     opacity={dimmed ? 0.4 : 1}
                     transform={`translate(${node.x}, ${node.y})`}
                     onClick={(e) => { e.stopPropagation(); setSelected(node.id); }}>
                    <rect className="rm-node-rect" width={NODE_W} height={h}/>
                    {/* Header band */}
                    <path d={`M 0 8 Q 0 0 8 0 L ${NODE_W - 8} 0 Q ${NODE_W} 0 ${NODE_W} 8 L ${NODE_W} ${HEADER_H} L 0 ${HEADER_H} Z`}
                          fill={headerColor}/>
                    <text x={14} y={HEADER_H / 2} className="rm-node-header">
                      {node.label}
                    </text>
                    {node.sub && (
                      <text x={NODE_W - 14} y={HEADER_H / 2} textAnchor="end" className="rm-node-header" style={{ opacity: 0.65, fontWeight: 500, fontSize: 10 }}>
                        {node.sub}
                      </text>
                    )}
                    {/* Body */}
                    {node.fields ? (
                      <>
                        <rect className="rm-node-fields-bg" x={0} y={HEADER_H} width={NODE_W} height={h - HEADER_H}/>
                        {node.fields.map((f, i) => (
                          <g key={i} transform={`translate(0, ${HEADER_H + 4 + i * FIELD_H})`}>
                            <text x={14} y={FIELD_H / 2} className={"rm-node-field " + (f.pk ? "pk" : "")}>
                              {f.pk ? "🔑 " : f.fk ? "↗ " : ""}{f.k}
                            </text>
                            <text x={NODE_W - 14} y={FIELD_H / 2} textAnchor="end" className="rm-node-field" style={{ opacity: 0.6 }}>
                              {f.t}
                            </text>
                          </g>
                        ))}
                      </>
                    ) : (
                      <g>
                        <text x={NODE_W / 2} y={HEADER_H + 18} textAnchor="middle" className="rm-node-sub">
                          {node.kind === "agent" ? "AI agent" : node.kind === "source" ? "data source" : node.kind === "data" ? "input" : "entity"}
                        </text>
                      </g>
                    )}
                  </g>
                );
              })}
            </g>
          </svg>

          {/* Zoom controls */}
          <div className="rm-zoom">
            <button title="Zoom in" onClick={zoomIn}><Icon name="plus" size={14}/></button>
            <button title="Zoom out" onClick={zoomOut}><Icon name="minus" size={14}/></button>
            <button title="Reset" onClick={zoomReset}><Icon name="grid" size={13}/></button>
          </div>

          {/* Legend */}
          <div className="rm-legend">
            <div className="rm-legend-title">Легенда · {data.label}</div>
            {data.legend.map((l, i) => (
              <div key={i} className="rm-legend-row">
                <span className="rm-legend-swatch" style={{ background: l.color, opacity: l.dash ? 0.6 : 1 }}/>
                <span>{l.label}</span>
              </div>
            ))}
            <div className="rm-legend-row" style={{ marginTop: 6, fontSize: 10, color: "var(--fg-3)" }}>
              ⌘+колесо — zoom · drag фон — pan
            </div>
          </div>

          {/* Side panel — node details */}
          {selectedNode && (
            <div className="rm-side">
              <div className="rm-side-head">
                <div>
                  <div className="rm-side-tag">{selectedNode.sub || data.label}</div>
                  <h3>{selectedNode.label}</h3>
                </div>
                <button className="icon-btn" onClick={() => setSelected(null)}>
                  <Icon name="x" size={14}/>
                </button>
              </div>
              <p>
                {mode === "business"   && businessDesc(selectedNode.id)}
                {mode === "technical"  && technicalDesc(selectedNode.id)}
                {mode === "integration"&& integrationDesc(selectedNode.id)}
                {mode === "ai"         && aiDesc(selectedNode.id)}
              </p>
              {selectedNode.fields && selectedNode.fields.length > 0 && (
                <>
                  <div className="rm-side-section">Поля</div>
                  <div className="rm-side-list">
                    {selectedNode.fields.map((f, i) => (
                      <div key={i} className="rm-side-list-item">
                        {f.pk && <span style={{ color: "var(--sorp-red)", fontWeight: 700 }}>PK</span>}
                        {f.fk && <span style={{ color: "#2C5C8C", fontWeight: 700 }}>FK</span>}
                        <code>{f.k}</code>
                        <span style={{ marginLeft: "auto", color: "var(--fg-3)", fontSize: 11 }}>{f.t}</span>
                      </div>
                    ))}
                  </div>
                </>
              )}
              {(incoming.length > 0 || outgoing.length > 0) && (
                <>
                  <div className="rm-side-section">Связи</div>
                  <div className="rm-side-list">
                    {incoming.map((e, i) => (
                      <div key={"in" + i} className="rm-side-list-item">
                        <Icon name="arrow_right" size={11}/>
                        <code>{e.from}</code>
                        <span style={{ marginLeft: "auto", color: "var(--fg-3)" }}>{e.label || "→"}</span>
                      </div>
                    ))}
                    {outgoing.map((e, i) => (
                      <div key={"out" + i} className="rm-side-list-item">
                        <Icon name="arrow_right" size={11}/>
                        <code>{e.to}</code>
                        <span style={{ marginLeft: "auto", color: "var(--fg-3)" }}>{e.label || "→"}</span>
                      </div>
                    ))}
                  </div>
                </>
              )}
            </div>
          )}
        </div>
      </div>
    );
  }

  // ─────────────────────────────────────────────────────────────
  // Per-mode descriptions
  // ─────────────────────────────────────────────────────────────
  function businessDesc(id) {
    return {
      company:  "Юридическое или физическое лицо, с которым у нас отношения. Может быть заказчиком, поставщиком или обоими.",
      contact:  "Физическое лицо — представитель Company. Email, телефон, мессенджеры, правила общения.",
      user:     "Сотрудник SORP с ролью (PM, закупщик, юрист, СБ, логист).",
      deal:     "Сделка SORP-XXXX — ядро всей системы. Linked to Amo (lead), Asana (task), Drive (folder).",
      task:     "Задача в Asana — у каждой сделки 0..N задач. ПМ и закупщик ведут диалог в её комментариях.",
      invoice:  "Счёт на оплату — выставляется заказчику либо приходит от поставщика.",
      payment:  "Денежная операция — связана со счётом, имеет direction (incoming / outgoing) и валюту.",
      document: "Файл (PDF/JPG/DOCX), привязанный к сделке или контракту. Тип распознан ИИ при загрузке.",
    }[id] || "—";
  }
  function technicalDesc(id) {
    return {
      counterparties: "uuid-keyed таблица контрагентов. enum type ∈ customer/supplier/both. Триггер обновляет updated_at.",
      deals:          "115 колонок, parent_deal_id для альтернативных закупок (Китай vs МИР). margin_amount/margin_pct — computed always stored. version авто-инкрементируется.",
      deal_items:     "Позиции в сделке: name, quantity, unit, unit_price, currency. Несколько на одну deal.",
      contracts:      "Договор с одной из сторон сделки (party_type customer/supplier). Содержит финансовые поля + контролирует статус.",
      payments:       "Транзакции по contracts. payment_type, direction, status (not_paid/prepaid/...). Связь с contract.",
      transitions_log:"APPEND-ONLY. Каждый переход current_stage пишется триггером. Источник истины для реестров.",
      employees:      "Наши сотрудники. Триггер на updated_at. Может быть FK во многих ролях (pm_id, buyer_id, ...).",
      documents:      "URL + storage_path + doc_type enum. Привязка либо к deal_id, либо к contract_id (CHECK).",
      shipments:      "Отгрузки с маршрутом и таможенной пошлиной. CHECK contract_id IS NOT NULL OR deal_id IS NOT NULL.",
    }[id] || "—";
  }
  function integrationDesc(id) {
    return {
      amocrm:      "Источник лидов и сделок. Webhook → leads_created/leads_status_changed → upsert в deals.",
      asana:       "Operational hub: 40 custom fields + key info в комментариях. PAT-токен, polling каждые 5 мин + webhook.",
      sheets:      "Google Sheets через Drive API. Импорт операционных таблиц (платежи, реестры, контракты) с предпросмотром в очереди.",
      bank:        "Банковская выписка в CSV. Авто-разбор + reconciliation agent сопоставляет с invoices.",
      email:       "IMAP. Письма от заказчиков и поставщиков; attachments извлекаются в documents.",
      deals:       "Основная таблица — наполняется из AmoCRM (через leads) и Asana (через GID и custom fields).",
      tasks:       "Каждая задача Asana = одна строка. SUB полей мапится в нашу нормальную схему.",
      fin_staging: "Промежуточная таблица перед нормализацией. Сюда падает результат парсинга Google Sheet до ручной верификации.",
      bank_txn:    "Транзакции из выписки. Используется reconciliation-агентом для сопоставления с invoices.",
      documents:   "Унифицированная таблица файлов — получает контент из Drive (через folder ID на deals) и Email (attachments).",
    }[id] || "—";
  }
  function aiDesc(id) {
    return {
      raw_text:       "Комментарии Asana, тело писем, заметки ПМ — всё, где может прятаться структурированная инфа (supplier, price, blocker, deadline).",
      raw_pdf:        "Сканы спецификаций, КП, инвойсов, таможенных деклараций — извлекаются OCR + LLM.",
      csv_data:       "Banking, finance — структурированные данные с лёгкой нормализацией.",
      duplicates:     "Кандидаты на слияние от Dedup-агента — операторы подтверждают/отклоняют в очереди.",
      extraction:     "NER + regex + LLM. Достаёт supplier, price, currency, deadline, brand из неструктурированного текста.",
      matching:       "Связывает amo_lead ↔ asana_task ↔ drive_folder через GID, INN, email, phone. Confidence-score на пару.",
      dedup:          "Fuzzy match по name (Levenshtein) + INN + phone. Возвращает группы кандидатов с confidence.",
      scoring:        "Оценивает уверенность других агентов и risk-score (санкции, dual-use, parallel-import).",
      reconciliation: "Сопоставляет суммы между Amo, Asana, банком, нашим контрактом — flag если разъезд > tolerance.",
      summarization:  "Готовит еженедельный digest для руководителя + per-deal summary для аналитики и onboarding нового ПМ.",
      deals_out:      "Куда пишут extraction + reconciliation: fields сделки + audit-row в provenance.",
      provenance:     "Каждое поле от ИИ имеет source/confidence/raw_value — это и есть field_provenance.",
      merged_party:   "Результат работы Dedup-агента: одна counterparty с присоединёнными псевдонимами и историей.",
    }[id] || "—";
  }

  window.RelationshipMapScreen = RelationshipMapScreen;
})();
