/* global React, API, Bubble, DocFrame, EmptyDoc, Msg, AiProg, Btn, now */
// Массовое создание по таблице: общие вопросы (list-поля) отвечаем один раз,
// данные работников берём из перетащенной таблицы (CSV) — по строке на договор,
// и создаём N документов через реальный DataDoc render (или демо без ключа).

window.SCENARIOS = window.SCENARIOS || {};

// плоский список полей шаблона (с маской/длиной для валидации значений)
function flattenLocal(sides) {
  const out = [];
  (sides.sides || []).forEach((side, si) => (side.tattrFolders || []).forEach((folder) =>
    (folder.tattrs || []).slice().sort((a, b) => (a.priority ?? 0) - (b.priority ?? 0)).forEach((attr) =>
      out.push({ key: si + ':' + attr.id, si, id: attr.id, name: attr.name || attr.id, required: !!attr.required, type: attr.type, values: attr.values, mask: attr.mask, maxLength: attr.maxLength, regexp: attr.regexp || attr.pattern }))));
  return out;
}
const isChoice = (f) => f.type === 'list' && Array.isArray(f.values) && f.values.length > 0;

// маска DataDoc → регэксп (9/# — цифра, A/a — буква, * — любой символ, остальное литерал)
function maskToRe(mask) {
  let s = '^';
  for (const c of String(mask)) {
    if (c === '9' || c === '#') s += '\\d';
    else if (/[AaАа]/.test(c)) s += '[A-Za-zА-Яа-яЁё]';
    else if (c === '*') s += '.';
    else s += c.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
  }
  return new RegExp(s + '$');
}
// проходит ли значение ограничения поля (длина/маска/регэксп) — иначе не отправляем, чтобы DataDoc не отклонил
function valueFits(v, f) {
  v = String(v == null ? '' : v);
  if (!v) return true;
  if (f.maxLength && v.length > f.maxLength) return false;
  if (f.regexp) { try { if (!new RegExp(f.regexp).test(v)) return false; } catch (e) { /* игнор */ } }
  if (f.mask && /[9#Aa*]/.test(f.mask)) { try { if (!maskToRe(f.mask).test(v)) return false; } catch (e) { /* игнор */ } }
  return true;
}
// имя атрибута/поля из текста ошибки DataDoc («…атрибута Серия», «переменная X …»)
function parseAttrName(msg) {
  const s = String(msg || '');
  const m = s.match(/атрибут[а]?\s+["«]?([^"».;:\n]+?)["»]?\s*[.．]?\s*$/i)
    || s.match(/атрибут[а]?\s+["«]?([^"».;:\n]+)/i)
    || s.match(/переменн\w*\s+(?:с\s+кодом\s+)?["«]?([^"».;:\n]+)/i)
    || s.match(/поле[йя]?\s+["«]?([^"».;:\n]+)/i);
  return m ? m[1].trim() : null;
}
const normName = (s) => String(s || '').toLowerCase().replace(/ё/g, 'е').replace(/\s+/g, ' ').trim();

// ── парсер CSV (авто-разделитель ; , tab, кавычки, BOM) ──
function parseCSV(text) {
  text = String(text || '').replace(/^﻿/, '');
  const head = text.split(/\r?\n/)[0] || '';
  const cnt = (d) => head.split(d).length;
  const delim = cnt(';') > cnt(',') ? ';' : (cnt('\t') > cnt(',') ? '\t' : ',');
  const rows = []; let row = [], cur = '', inq = false;
  for (let i = 0; i < text.length; i++) {
    const c = text[i];
    if (inq) { if (c === '"') { if (text[i + 1] === '"') { cur += '"'; i++; } else inq = false; } else cur += c; }
    else if (c === '"') inq = true;
    else if (c === delim) { row.push(cur); cur = ''; }
    else if (c === '\n') { row.push(cur); rows.push(row); row = []; cur = ''; }
    else if (c === '\r') { /* skip */ }
    else cur += c;
  }
  if (cur !== '' || row.length) { row.push(cur); rows.push(row); }
  return rows.map((r) => r.map((c) => c.trim())).filter((r) => r.some((c) => c !== ''));
}
const norm = (s) => (s || '').toString().toLowerCase().replace(/ё/g, 'е').replace(/[^а-яa-z0-9]/g, '');
function toISO(v) {
  const m = String(v || '').trim().match(/^(\d{1,2})[.\/-](\d{1,2})[.\/-](\d{2,4})$/);
  if (!m) return v; let y = m[3]; if (y.length === 2) y = '20' + y;
  return `${y}-${m[2].padStart(2, '0')}-${m[1].padStart(2, '0')}`;
}
// сопоставление столбцов таблицы с полями шаблона (по названию)
function autoMap(headers, dataFields) {
  const H = headers.map(norm); const map = {};
  dataFields.forEach((f) => {
    const fn = norm(f.name);
    let idx = H.findIndex((h) => h === fn);
    if (idx < 0) idx = H.findIndex((h) => h && (h.includes(fn) || fn.includes(h)) && Math.min(h.length, fn.length) >= 4);
    if (idx >= 0) map[f.key] = idx;
  });
  return map;
}
// добор непонятных столбцов через Claude (консервативно: лучше null, чем неверно)
const MAP_SYS = `Сопоставь столбцы таблицы с полями шаблона документа. Дают headers (названия столбцов по индексам) и fields ({id,name}). Верни СТРОГО JSON {"map":{"<id>":<индекс столбца или null>}}. ПРАВИЛА: сопоставляй только при явном совпадении смысла. НЕ сопоставляй поля разных документов (паспорт, патент, ДМС, СНИЛС, ИНН — разные сущности; «Серия/Номер патента» ≠ паспорт). Комбинированный столбец «Паспорт (серия и номер)» НЕ сопоставляй с отдельными полями «Серия»/«Номер». Если нет однозначного столбца — ставь null. Без markdown.`;
async function claudeMap(headers, fields) {
  try {
    const r = await API.claudeJSON(MAP_SYS, [{ type: 'text', text: 'headers:\n' + JSON.stringify(headers) + '\nfields:\n' + JSON.stringify(fields.map((f) => ({ id: f.key, name: f.name }))) }], { max_tokens: 800 });
    return (r.data && r.data.map) || {};
  } catch (e) { return {}; }
}
function templateCSV(dataFields) {
  const head = dataFields.map((f) => f.name).join(';');
  const ex = dataFields.map((f) => f.type === 'date' ? '01.09.2026' : (/money|integer|decimal/.test(f.type) ? '50000' : 'Иванов Иван Иванович')).join(';');
  return '﻿' + head + '\r\n' + ex + '\r\n';
}

function BatchBuilder({ ctx, tpl, sides, choiceFields, dataFields }) {
  const [choice, setChoice] = React.useState(() => { const o = {}; choiceFields.forEach((f) => o[f.key] = (f.values && f.values[0]) || ''); return o; });
  const [headers, setHeaders] = React.useState(null);
  const [rows, setRows] = React.useState([]);
  const [map, setMap] = React.useState({});
  const [common, setCommon] = React.useState({}); // общие поля (нет в таблице) — один раз для всех
  const [busy, setBusy] = React.useState(false);
  const [prog, setProg] = React.useState(null);
  const [results, setResults] = React.useState(null);
  const [signed, setSigned] = React.useState(false);
  const fileRef = React.useRef(null);

  // выбор организации из Дадаты в общем поле → заполнить связанные общие поля (реквизиты, директор ФИО, город)
  const applyPartyCommon = (party) => {
    const inc = {};
    (dataFields.filter((f) => map[f.key] == null)).forEach((f) => {
      const k = window.fieldKind ? window.fieldKind(f.name) : null;
      const n = (f.name || '').toLowerCase();
      let v = '';
      if (k === 'company') v = (party.full && party.full !== '—') ? party.full : party.value;
      else if (k === 'inn') v = party.inn || '';
      else if (k === 'kpp') v = party.kpp || '';
      else if (k === 'ogrn') v = party.ogrn || '';
      else if (k === 'address') v = (party.address && party.address !== '—') ? party.address : '';
      else if (/город|населённый пункт|населенный пункт/.test(n)) { const m = (party.address || '').match(/г\.?\s*([А-ЯЁ][а-яё-]+)/); v = m ? m[1] : ''; }
      else if (/директор|руководител|генеральн|подписант/.test(n)) {
        const p = (party.director || '').replace('—', '').trim().split(/\s+/);
        if (/фамили/.test(n)) v = p[0] || ''; else if (/отчеств/.test(n)) v = p[2] || ''; else if (/\bимя\b|имя /.test(n)) v = p[1] || ''; else v = (party.director && party.director !== '—') ? party.director : '';
      }
      if (v) inc[f.key] = v;
    });
    if (Object.keys(inc).length) setCommon((o) => ({ ...o, ...inc }));
  };

  const applyText = async (text, name) => {
    const all = parseCSV(text);
    if (all.length < 2) { ctx.ai('В таблице нет строк с данными — нужна строка-заголовок и хотя бы одна строка сотрудника.'); return; }
    const anim = ctx.node(<Msg who="ai" ts={now()}><CsvParsing label="Разбираю таблицу" /></Msg>);
    const hd = all[0], dr = all.slice(1);
    const m = autoMap(hd, dataFields);
    const unmatched = dataFields.filter((f) => m[f.key] == null);
    if (unmatched.length) { const cm = await claudeMap(hd, unmatched); Object.keys(cm).forEach((k) => { if (cm[k] != null && m[k] == null) m[k] = cm[k]; }); }
    await sleep(700); // дать анимации разбора проиграться
    ctx.remove(anim);
    // отбрасываем сопоставления, где значения не проходят маску/длину поля (иначе DataDoc отклонит договор)
    let dropped = 0;
    Object.keys(m).forEach((k) => {
      const f = dataFields.find((x) => x.key === k); const ci = m[k];
      const sample = dr.find((r) => r[ci] != null && String(r[ci]).trim() !== '');
      if (f && sample && !valueFits(sample[ci], f)) { delete m[k]; dropped++; }
    });
    setHeaders(hd); setRows(dr); setMap(m); setResults(null); setSigned(false);
    ctx.aiNode(<Bubble>Загрузила таблицу <b>{name || 'данные'}</b>: <b>{dr.length}</b> сотрудников, сопоставила <b>{Object.keys(m).length}/{dataFields.length}</b> полей. Проверьте общие условия и нажмите «Создать договоры».{dropped ? <div style={{ fontSize: 11, color: 'var(--ink-3)', marginTop: 4 }}>{dropped} {dropped === 1 ? 'столбец пропущен' : 'столбцов пропущено'}: значения не подходят под формат поля (напр. маска серии/номера) — при необходимости поправьте столбцы в таблице.</div> : null}</Bubble>);
  };
  const loadFile = (file) => { const r = new FileReader(); r.onload = (e) => applyText(e.target.result, file.name); r.onerror = () => ctx.ai('Не удалось прочитать файл.'); r.readAsText(file, 'utf-8'); };
  React.useEffect(() => { window.__activeBatch = { loadFile }; return () => { if (window.__activeBatch && window.__activeBatch.loadFile === loadFile) window.__activeBatch = null; }; });

  // разумные дефолты для типовых общих полей (уменьшают список обязательных)
  React.useEffect(() => {
    if (!headers) return;
    setCommon((prev) => {
      const d = { ...prev };
      dataFields.filter((f) => map[f.key] == null).forEach((f) => {
        if (d[f.key]) return; const n = (f.name || '').toLowerCase();
        if (/страна граждан/.test(n)) d[f.key] = 'Российская Федерация';
        else if (/продолжительность рабочего времени/.test(n)) d[f.key] = '40';
        else if (/продолжительность ежедневн/.test(n)) d[f.key] = '8';
        else if (/время начала/.test(n)) d[f.key] = '09:00';
        else if (/время окончания/.test(n)) d[f.key] = '18:00';
        else if (/количество ставок/.test(n)) d[f.key] = '1';
        else if (/районный коэффициент/.test(n)) d[f.key] = '0';
        else if (/число выплаты первой половин|аванс/.test(n)) d[f.key] = '20';
        else if (/число выплаты (заработной платы|зарплаты)/.test(n)) d[f.key] = '5';
      });
      return d;
    });
  }, [headers, map]);

  const dlTemplate = () => {
    const blob = new Blob([templateCSV(dataFields)], { type: 'text/csv;charset=utf-8' });
    const u = URL.createObjectURL(blob); const a = document.createElement('a');
    a.href = u; a.download = (tpl.name || 'шаблон') + ' — таблица.csv'; document.body.appendChild(a); a.click(); a.remove();
    setTimeout(() => URL.revokeObjectURL(u), 4000);
  };
  // поля имени работника (ФИО одним полем или отдельные Фамилия/Имя/Отчество; директора исключаем)
  const nameFields = dataFields.filter((f) => /фио|фамили|(^|[^а-я])имя|отчеств/i.test(f.name) && !/директор|руководител|генеральн|представит/i.test(f.name));
  const rowFio = (row) => nameFields.map((f) => (map[f.key] != null ? row[map[f.key]] : '')).filter(Boolean).join(' ').trim();
  const rowName = (row) => tpl.name + (rowFio(row) ? ' — ' + rowFio(row) : '');

  // скачивание готового договора из DataDoc (PDF/Word) по renderId
  const downloadDoc = async (c, fmt) => {
    if (!c || !c.id) return;
    const r = await API.datadoc('GET', `/templates/${encodeURIComponent(tpl.id)}/render/${encodeURIComponent(c.id)}?format=${fmt}`, null, { raw: true });
    if (!r.ok) throw new Error('HTTP ' + r.status);
    const url = URL.createObjectURL(await r.blob());
    const a = document.createElement('a');
    a.href = url; a.download = String(c.name || c.fio || 'договор').replace(/[\\/:*?"<>|]/g, ' ').slice(0, 90) + (fmt === 'docx' ? '.docx' : '.pdf');
    document.body.appendChild(a); a.click(); a.remove();
    setTimeout(() => URL.revokeObjectURL(url), 6000);
  };
  const [dlState, setDlState] = React.useState('');
  const downloadAll = async (fmt) => {
    const withId = (results ? results.list : []).filter((o) => o.id && !o.error);
    if (!withId.length) return;
    setDlState('идёт');
    let done = 0, err = 0;
    for (const c of withId) { try { await downloadDoc(c, fmt); done++; } catch (e) { err++; } await new Promise((r) => setTimeout(r, 450)); }
    setDlState('');
    ctx.ai(`Скачала ${done} из ${withId.length} ${fmt === 'docx' ? 'Word' : 'PDF'}${err ? `, не удалось: ${err}` : ''}. Все договоры также сохранены в вашем аккаунте DataDoc — раздел «Документы».`);
  };

  const batchDoc = (list, st, demo) => (
    <DocFrame title={list.length + ' договоров'} sub={st ? 'подписаны УКЭП · отправлены работникам' : (demo ? 'демо · черновики' : 'создано в DataDoc')}>
      <div className="docp" style={{ fontFamily: 'inherit', fontSize: 12 }}>
        <div className="bdg" style={st ? { background: '#d4ecdb', borderColor: '#1f6b3a' } : {}}>{st ? 'подписано ✓' : 'черновики'}</div>
        {list.map((o, i) => <div className="cprow" key={i}><span className="k">{o.number || ('№ ' + (i + 1))}</span><span className="v" style={o.error ? { color: '#c33' } : {}}>{o.fio || '—'}{o.error ? ' · ошибка' : (o.id ? <span onClick={() => downloadDoc(o, 'pdf').catch(() => ctx.ai('Не удалось скачать PDF.'))} style={{ color: 'var(--blue)', cursor: 'pointer', marginLeft: 8 }}>PDF ↓</span> : null)}</span></div>)}
      </div>
    </DocFrame>
  );

  const create = async () => {
    // не блокируем: многие «обязательные» в шаблоне поля условны (патент/ДМС для иностранцев,
    // поля срочного/испытания) — пусть решает DataDoc, ошибки по строкам покажем как есть
    setBusy(true); const total = rows.length; setProg({ done: 0, total, fail: 0 });
    const anim = ctx.node(<Msg who="ai" ts={now()}><DocsCreating label={`Создаю ${total} договоров`} count={Math.min(6, Math.max(3, total))} /></Msg>);
    const token = API.getToken && API.getToken(); const demo = !!tpl.demo || !token;
    const out = []; let fail = 0; const autoDropped = new Set();
    const allFields = [...dataFields, ...choiceFields];

    // рендер строки с адаптивным повтором: DataDoc называет проблемный атрибут → убираем его значение и пробуем снова
    const renderRow = async (name, sideValues) => {
      for (let attempt = 0; attempt < 10; attempt++) {
        try { return await API.datadoc('POST', `/templates/${encodeURIComponent(tpl.id)}/render`, { name, sideValues }); }
        catch (e) {
          const attr = parseAttrName(e.message); let removed = false;
          if (attr) {
            const fld = allFields.find((f) => normName(f.name) === normName(attr))
              || allFields.find((f) => normName(f.name).includes(normName(attr)) || normName(attr).includes(normName(f.name)));
            if (fld && sideValues[fld.si] && Object.prototype.hasOwnProperty.call(sideValues[fld.si].tattrValues, fld.id)) {
              delete sideValues[fld.si].tattrValues[fld.id]; removed = true; autoDropped.add(fld.name);
            }
          }
          if (!removed) throw e; // не смогли определить/убрать поле — отдаём ошибку как есть
        }
      }
      throw new Error('слишком много несовместимых полей');
    };

    for (let i = 0; i < total; i++) {
      const row = rows[i];
      const sideValues = (sides.sides || []).map(() => ({ tattrValues: {} }));
      choiceFields.forEach((f) => { if (choice[f.key]) sideValues[f.si].tattrValues[f.id] = choice[f.key]; });
      dataFields.forEach((f) => {
        const ci = map[f.key];
        let v = ci != null ? row[ci] : common[f.key]; // из таблицы — иначе общее поле
        if (v == null || v === '') return;
        if (f.type === 'date') v = toISO(v);
        if (!valueFits(v, f)) return; // не отправляем значение, не проходящее маску/длину поля
        sideValues[f.si].tattrValues[f.id] = v;
      });
      const name = rowName(row);
      try {
        if (demo) { await new Promise((r) => setTimeout(r, 110)); out.push({ number: 'ТД-' + (101 + i), fio: rowFio(row) || name }); }
        else { const cr = await renderRow(name, sideValues); out.push({ number: cr.renderNumber != null ? cr.renderNumber : '—', name: cr.renderName || name, id: cr.renderId || cr.id, fio: rowFio(row) || name }); }
      } catch (e) { fail++; out.push({ fio: rowFio(row) || name, error: e.message }); }
      setProg({ done: i + 1, total, fail });
    }
    ctx.remove(anim);
    setBusy(false); setResults({ list: out, fail, demo, autoDropped: [...autoDropped] });
    const ok = out.filter((o) => !o.error).length;
    if (ok) ctx.charge(ok);
    ctx.setDoc(batchDoc(out, false, demo));
    const firstErr = out.find((o) => o.error);
    const dropped = [...autoDropped];
    ctx.aiNode(<Bubble>
      <span style={{ display: 'flex', gap: 8, alignItems: 'center' }}><span style={{ fontSize: 24, color: fail && !ok ? '#c33' : '#1f6b3a' }}>{fail && !ok ? '⚠' : '✓'}</span><span>Готово. Создала <b>{ok}</b> {ok % 10 === 1 && ok % 100 !== 11 ? 'договор' : 'договоров'}{fail ? `, ошибок: ${fail}` : ''}{demo ? ' (демо — без DataDoc-ключа документы не сохраняются в аккаунт)' : ' в DataDoc'}. Списано <b>{ok} доксов</b>.</span></span>
      {dropped.length ? <div style={{ fontSize: 11, color: '#8a5a00', marginTop: 6 }}>Чтобы договоры прошли, автоматически исключила поля с несовместимым форматом: <b>{dropped.join(', ')}</b>. Если они нужны в договоре — задайте им правильный формат: отдельным столбцом в таблице (напр. «Серия» — 4 цифры, «Номер» — 6 цифр) или в «Общих полях», и создайте заново.</div> : null}
      {fail && firstErr ? <div style={{ fontSize: 11, color: '#c33', marginTop: 6 }}>DataDoc отклонил (пример): {String(firstErr.error).slice(0, 220)}.</div> : null}
    </Bubble>);
  };

  const sign = () => { setSigned(true); const ok = results.list.filter((o) => !o.error); ctx.setDoc(batchDoc(results.list, true, results.demo)); ctx.aiNode(<Bubble><span style={{ display: 'flex', gap: 8, alignItems: 'center' }}><span style={{ fontSize: 24, color: '#1f6b3a' }}>✓</span><span><b>{ok.length} договоров</b> подписаны вашей УКЭП и отправлены работникам на подписание пачкой.</span></span></Bubble>); };

  const matched = Object.keys(map).length;
  const SF = window.SmartField;
  const commonFields = dataFields.filter((f) => map[f.key] == null); // нет в таблице → спрашиваем один раз
  const reqMissing = commonFields.filter((f) => f.required && !(common[f.key] != null && String(common[f.key]).trim() !== ''));
  const commonSorted = commonFields.slice().sort((a, b) => (b.required ? 1 : 0) - (a.required ? 1 : 0));
  return (
    <div className="cpcard" style={{ maxWidth: 540 }}>
      <div className="h"><div className="nm">{tpl.name}</div><div className="ok">{choiceFields.length} вопросов · {dataFields.length} полей</div></div>

      <div style={{ fontSize: 12, color: 'var(--ink-2)', margin: '4px 0 8px' }}>Общие условия — применю ко всем договорам:</div>
      <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
        {choiceFields.map((f) => (
          <div key={f.key}>
            <div className="k" style={{ fontSize: 11, marginBottom: 3 }}>{f.name}</div>
            <select value={choice[f.key] || ''} disabled={!!results} onChange={(e) => setChoice((o) => ({ ...o, [f.key]: e.target.value }))}
              style={{ width: '100%', padding: '6px 8px', border: '1px solid var(--line-2)', borderRadius: 6, fontSize: 12.5, fontFamily: 'var(--sans)', background: 'var(--paper)' }}>
              {(f.values || []).map((v, i) => <option key={i} value={v}>{v}</option>)}
            </select>
          </div>
        ))}
      </div>

      <div style={{ borderTop: '1px solid var(--line)', margin: '12px 0 10px' }} />
      {!results ? (
        !rows.length ? (
          <div>
            <div style={{ fontSize: 12.5, color: 'var(--ink-2)', marginBottom: 8 }}>Данные работников — из таблицы: по строке на сотрудника. Скачайте шаблон, заполните и перетащите файл в чат (📎).</div>
            <div className="acta">
              <Btn kind="dashed" onClick={dlTemplate}>Скачать шаблон таблицы (CSV) ↓</Btn>
              <div className="btn-aip" onClick={() => fileRef.current && fileRef.current.click()}>Загрузить таблицу…</div>
              <input ref={fileRef} type="file" accept=".csv,.txt,.tsv,text/csv" style={{ display: 'none' }} onChange={(e) => { const f = e.target.files && e.target.files[0]; if (f) loadFile(f); e.target.value = ''; }} />
            </div>
            <div style={{ fontSize: 11, color: 'var(--ink-3)', marginTop: 8 }}>Столбцы: {dataFields.slice(0, 8).map((f) => f.name).join(', ')}{dataFields.length > 8 ? ' …' : ''}</div>
          </div>
        ) : (
          <div>
            <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 6 }}>
              <div style={{ fontSize: 13, fontWeight: 600 }}>{rows.length} сотрудников</div>
              <div style={{ fontSize: 11, color: matched === dataFields.length ? '#1f6b3a' : 'var(--ink-3)' }}>сопоставлено {matched}/{dataFields.length}</div>
            </div>
            <div style={{ maxHeight: 170, overflow: 'auto', border: '1px solid var(--line)', borderRadius: 8 }}>
              <table style={{ borderCollapse: 'collapse', fontSize: 11, width: '100%' }}>
                <thead><tr>{(headers || []).slice(0, 6).map((h, i) => <th key={i} style={{ textAlign: 'left', padding: '5px 8px', borderBottom: '1px solid var(--line)', position: 'sticky', top: 0, background: 'var(--paper-2)', whiteSpace: 'nowrap' }}>{h}</th>)}</tr></thead>
                <tbody>{rows.slice(0, 5).map((r, ri) => <tr key={ri}>{r.slice(0, 6).map((c, ci) => <td key={ci} style={{ padding: '4px 8px', borderBottom: '1px solid var(--line)', whiteSpace: 'nowrap', maxWidth: 140, overflow: 'hidden', textOverflow: 'ellipsis' }}>{c}</td>)}</tr>)}</tbody>
              </table>
            </div>
            {rows.length > 5 ? <div style={{ fontSize: 11, color: 'var(--ink-3)', marginTop: 4 }}>…и ещё {rows.length - 5}</div> : null}

            {commonFields.length ? (
              <div style={{ marginTop: 12, borderTop: '1px solid var(--line)', paddingTop: 10 }}>
                <div style={{ fontSize: 12, color: 'var(--ink-2)', marginBottom: 2 }}><b>Общие поля</b> — этих столбцов нет в таблице, заполните один раз, применю ко всем договорам:</div>
                <div style={{ fontSize: 11, color: 'var(--ink-3)', marginBottom: 8 }}>наименование, ИНН и адрес подскажет Дадата. Если поле различается по сотрудникам — добавьте столбец в таблицу.</div>
                <div style={{ maxHeight: 320, overflowY: 'auto', paddingRight: 4 }}>
                  {commonSorted.map((f) => SF
                    ? <SF key={f.key} f={f} value={common[f.key]} onChange={(v) => setCommon((o) => ({ ...o, [f.key]: v }))} onParty={applyPartyCommon} />
                    : (
                      <div key={f.key} style={{ marginBottom: 7 }}>
                        <div className="k" style={{ fontSize: 11, marginBottom: 2 }}>{f.name}{f.required ? <span style={{ color: '#c33' }}> *</span> : null}</div>
                        <input value={common[f.key] || ''} onChange={(e) => setCommon((o) => ({ ...o, [f.key]: e.target.value }))} style={{ width: '100%', padding: '6px 8px', border: '1px solid var(--line-2)', borderRadius: 6, fontSize: 12.5, fontFamily: 'var(--sans)' }} />
                      </div>
                    ))}
                </div>
              </div>
            ) : null}

            {reqMissing.length ? (
              <div style={{ fontSize: 11, color: '#8a5a00', background: '#fff7e6', border: '1px solid #ffe0a3', borderRadius: 8, padding: '7px 9px', marginTop: 8 }}>
                В шаблоне помечены обязательными и пока пусты <b>{reqMissing.length}</b> полей: {reqMissing.slice(0, 8).map((f) => f.name).join(', ')}{reqMissing.length > 8 ? ` и ещё ${reqMissing.length - 8}` : ''}. Заполните применимые; неприменимые (патент/ДМС для иностранцев, поля срочного договора и испытания) можно оставить пустыми — можно нажимать «Создать», DataDoc отклонит только то, что реально требуется при выбранных условиях.
              </div>
            ) : null}
            <div className="acta" style={{ marginTop: 10 }}>
              <Btn kind="primary" onClick={busy ? undefined : create}>{busy && prog ? `Создаю ${prog.done}/${prog.total}…` : `Создать ${rows.length} договоров →`}</Btn>
              {!busy ? <div className="btn-aip" onClick={() => { setRows([]); setHeaders(null); setMap({}); }}>Другая таблица</div> : null}
            </div>
          </div>
        )
      ) : (
        <div>
          <div className="acost"><span className="v">⬨ {results.list.filter((o) => !o.error).length} доксов</span><span>· {results.list.length} договоров{results.fail ? `, ошибок ${results.fail}` : ''}</span></div>
          {(() => { const withId = results.list.filter((o) => o.id && !o.error); return withId.length ? (
            <div style={{ marginTop: 8 }}>
              <div style={{ fontSize: 11, color: '#1f6b3a', marginBottom: 6 }}>✓ Сохранены в вашем аккаунте DataDoc — раздел «Документы». Можно скачать:</div>
              <div className="acta">
                <Btn kind="primary" onClick={dlState ? undefined : () => downloadAll('pdf')}>{dlState ? 'Скачиваю…' : `Скачать все PDF (${withId.length}) ↓`}</Btn>
                <div className="btn-aip" onClick={dlState ? undefined : () => downloadAll('docx')}>Скачать все Word ↓</div>
              </div>
            </div>
          ) : (results.demo ? <div style={{ fontSize: 11, color: 'var(--ink-3)', marginTop: 8 }}>Демо-режим: без DataDoc-ключа договоры не сохраняются и недоступны для скачивания — подключите ключ на реальном шаблоне.</div> : null); })()}
          {!signed ? (
            <div className="acta" style={{ marginTop: 10 }}>
              <Btn kind="primary" onClick={sign}>Подписать пачкой (УКЭП) →</Btn>
              <div className="btn-aip" onClick={() => { setResults(null); setSigned(false); }}>Ещё пачка</div>
            </div>
          ) : <div style={{ fontSize: 12, color: '#1f6b3a', marginTop: 8 }}>✓ Подписаны и отправлены работникам.</div>}
        </div>
      )}
    </div>
  );
}

// приём таблицы из чата (drag&drop / 📎) во время активного batch
async function batchTable(ctx, { text, files }) {
  const ab = window.__activeBatch;
  if (!ab) { ctx.uncapture(); return; }
  const t = (text || '').trim().toLowerCase();
  if (/^(отмена|стоп|выход|назад|отменить)/.test(t)) { ctx.uncapture(); window.__activeBatch = null; return ctx.ai('Ок, вышла из массового создания. Чем ещё помочь?'); }
  if (files && files.length) { const f = files.find((x) => /\.(csv|txt|tsv)$/i.test(x.name)) || files[0]; ab.loadFile(f); return; }
  ctx.ai('Перетащите таблицу с сотрудниками (CSV) в чат или нажмите «Загрузить таблицу». Нужен образец — кнопка «Скачать шаблон таблицы».');
}

async function openBatchFlow(ctx, tpl) {
  ctx.setHead('Массовое создание договоров', tpl.name);
  ctx.setDoc(<DocFrame title={tpl.name} sub="загружаю поля…"><EmptyDoc lines={['🗂', 'читаю поля шаблона…']} /></DocFrame>);
  let sides = tpl.sides || null;
  if (!sides) {
    const k = ctx.node(<Msg who="ai" ts={now()}><AiProg icon="🗂" label="читаю поля шаблона" count="DataDoc" /></Msg>);
    try { sides = await API.datadoc('GET', `/templates/${encodeURIComponent(tpl.id)}/sides`); }
    catch (e) { ctx.remove(k); ctx.aiNode(<Bubble><span style={{ color: '#c33' }}>Не удалось загрузить поля: {e.message}</span></Bubble>); return; }
    ctx.remove(k);
  }
  const fields = flattenLocal(sides);
  const choiceFields = fields.filter(isChoice);
  const dataFields = fields.filter((f) => !isChoice(f));
  ctx.setDoc(<DocFrame title={tpl.name} sub="массовое создание · таблица → договоры"><EmptyDoc lines={['🗂', 'ответьте на общие вопросы и перетащите таблицу —\nсоздам договор на каждого сотрудника']} /></DocFrame>);
  ctx.aiNode(
    <>
      <Bubble>
        Массовое создание по шаблону <b>«{tpl.name}»</b>. Как это работает:
        <ol style={{ margin: '8px 0 0', paddingLeft: 18, lineHeight: 1.6 }}>
          <li><b>ответьте на общие вопросы</b> ({choiceFields.length}) — они одинаковы для всех договоров;</li>
          <li>скачайте <b>шаблон таблицы</b>, впишите сотрудников (по строке) и <b>перетащите файл в чат</b> 📎;</li>
          <li>я создам <b>по договору на каждую строку</b> и предложу подписать пачкой УКЭП.</li>
        </ol>
      </Bubble>
      <BatchBuilder ctx={ctx} tpl={tpl} sides={sides} choiceFields={choiceFields} dataFields={dataFields} />
    </>
  );
  ctx.capture(batchTable);
}
window.SCENARIOS.batchTemplate = openBatchFlow;
