/* form-engine.jsx — unified FormEngine for measure_field (+ field-measure page).
   Канон: docs/superpowers/specs/2026-06-28-questionnaire-sets-measure-funnel-design.md · KSH-350 */
(function () {
  const { Badge, Button } = window.DesignSystem_9e0a09;
  const { Ic } = window;
  const { useState, useEffect, useCallback, useMemo, useRef } = React;

  const PT_LABELS = { residential: 'Жилая', house: 'Дом / таунхаус', commercial: 'Коммерция', apartment: 'Квартира' };

  function fmtTimer(ms) {
    const h = Math.floor(ms / 3600000);
    const m = Math.floor((ms % 3600000) / 60000);
    return h + ' ч ' + m + ' мин';
  }

  function parseQuery() {
    const q = new URLSearchParams(window.location.search);
    return {
      dealId: q.get('deal') || 'D-404',
      formToken: q.get('form') || '',
      role: q.get('role') || '',
      previewSchema: q.get('previewSchema') || '',
    };
  }

  function MediaFieldInput({ field, value, onChange, disabled }) {
    const id = field.id;
    const normalize = window.msNormalizeMediaField || ((f) => f);
    const f = normalize(field);
    const list = Array.isArray(value) ? value : [];
    const max = f.mediaMultiple === false ? 1 : (f.mediaMaxFiles || 10);
    const kinds = f.mediaKinds || ['photo', 'video'];
    const fileRef = useRef(null);
    const pendingKindRef = useRef('photo');

    const remove = (mid) => onChange(id, list.filter((m) => m.id !== mid));

    const openPicker = (kind) => {
      if (disabled || list.length >= max) return;
      pendingKindRef.current = kind;
      if (fileRef.current) {
        fileRef.current.accept = kind === 'video' ? 'video/*' : 'image/*';
        fileRef.current.multiple = f.mediaMultiple !== false && max - list.length > 1;
        fileRef.current.click();
      }
    };

    const [busy, setBusy] = useState(false);
    const onFiles = async (e) => {
      const files = Array.from(e.target.files || []);
      e.target.value = '';
      if (!files.length || disabled) return;
      const kind = pendingKindRef.current || 'photo';
      const room = max - list.length;
      const slice = f.mediaMultiple === false ? files.slice(0, 1) : files.slice(0, room);
      let uploaded = [];
      if (window.Files && window.Files.uploadAll) {
        setBusy(true);
        try { uploaded = await window.Files.uploadAll(slice, { pid: f.uploadPid || 'forms', kind: f.uploadKind || 'media' }); } catch (err) { uploaded = []; }
        setBusy(false);
      }
      const next = slice.map((file, i) => ({
        id: 'm-' + Math.random().toString(36).slice(2, 8),
        label: file.name || (kind === 'video' ? 'Видео' : 'Фото') + ' ' + (list.length + i + 1),
        kind,
        fileName: file.name,
        fileSize: file.size,
        mimeType: file.type,
        url: uploaded[i] ? uploaded[i].url : undefined,
        stub: false,
      }));
      onChange(id, f.mediaMultiple === false ? next.slice(0, 1) : [...list, ...next]);
    };

    return (
      <div>
        <input
          ref={fileRef}
          type="file"
          style={{ display: 'none' }}
          onChange={onFiles}
          disabled={disabled}
        />
        <div className={'fe-media-list' + (list.some((m) => m.url && m.kind !== 'video') ? ' fe-media-list--thumbs' : '')}>
          {list.map((m) => (
            <div key={m.id} className={'fe-media-row' + (m.url && m.kind !== 'video' ? ' fe-media-row--thumb' : '')} style={m.url && m.kind !== 'video' ? { backgroundImage: 'url(' + m.url + ')' } : undefined}>
              {!(m.url && m.kind !== 'video') && <Ic n={m.kind === 'video' ? 'video' : 'image'} s={14} />}
              <span style={{ flex: 1 }} className={m.url && m.kind !== 'video' ? 'fe-media-cap' : ''}>{m.label || m.fileName || 'Файл'}</span>
              {!disabled && (
                <button type="button" className="fe-chip" style={{ minHeight: 32, padding: '4px 10px' }} onClick={() => remove(m.id)}>×</button>
              )}
            </div>
          ))}
        </div>
        {!disabled && list.length < max && (
          <div style={{ display: 'flex', gap: 8, marginTop: 8, flexWrap: 'wrap' }}>
            {kinds.includes('photo') && (
              <button type="button" className="fe-media-add" disabled={busy} onClick={() => openPicker('photo')}><Ic n="camera" s={14} /> {busy ? 'Загружаю…' : 'Фото'}</button>
            )}
            {kinds.includes('video') && (
              <button type="button" className="fe-media-add" onClick={() => openPicker('video')}><Ic n="video" s={14} /> Видео</button>
            )}
          </div>
        )}
        {max > 1 && (
          <div style={{ marginTop: 6, fontSize: 11.5, color: 'var(--text-tertiary, #8a817a)' }}>
            {list.length} / {max} файлов
          </div>
        )}
      </div>
    );
  }

  /* «+ свой вариант» — у каждого вопроса с вариантами: свой ответ добавляется как выбранный чип (хранится прямо в ответе) */
  function AddOption({ disabled, onAdd }) {
    const [open, setOpen] = useState(false);
    const [v, setV] = useState('');
    if (disabled) return null;
    const cancel = () => { setOpen(false); setV(''); };
    const commit = () => { const t = v.trim(); if (t) onAdd(t); cancel(); };
    if (!open) return <button type="button" className="fe-chip fe-chip--add" onClick={() => setOpen(true)} title="Добавить свой вариант"><Ic n="plus" s={13} /><span>свой вариант</span></button>;
    return (
      <span className="fe-chip-add">
        <input className="fe-input" autoFocus value={v} placeholder="Свой вариант…" onChange={(e) => setV(e.target.value)}
          onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); commit(); } if (e.key === 'Escape') cancel(); }} />
        <button type="button" className="fe-chip fe-chip--ok" onClick={commit} aria-label="Добавить"><Ic n="check" s={13} /></button>
        <button type="button" className="fe-chip" onClick={cancel} aria-label="Отмена"><Ic n="x" s={13} /></button>
      </span>
    );
  }
  function ChipsInput({ field, value, onChange, disabled }) {
    const id = field.id;
    const arr = Array.isArray(value) ? value : [];
    const opts = (field.options || []).map((o) => (typeof o === 'object' ? o.l || o.v : o));
    const extra = arr.filter((x) => !opts.includes(x));
    const legacy = field.allowCustom && field.customId && field.customValue ? String(field.customValue) : '';
    const toggle = (o) => onChange(id, arr.includes(o) ? arr.filter((x) => x !== o) : [...arr, o]);
    return (
      <div className="fe-chips">
        {opts.map((o) => (
          <button key={o} type="button" className={'fe-chip' + (arr.includes(o) ? ' on' : '')} disabled={disabled} onClick={() => toggle(o)}>{o}</button>
        ))}
        {extra.map((o) => (
          <button key={'x-' + o} type="button" className="fe-chip on fe-chip--custom" disabled={disabled} onClick={() => toggle(o)} title="Свой вариант — нажмите, чтобы убрать">{o}<Ic n="x" s={12} /></button>
        ))}
        {legacy && <button type="button" className="fe-chip on fe-chip--custom" disabled={disabled} onClick={() => onChange(field.customId, '')} title="Свой вариант — нажмите, чтобы убрать">{legacy}<Ic n="x" s={12} /></button>}
        <AddOption disabled={disabled} onAdd={(t) => { if (!arr.includes(t)) onChange(id, [...arr, t]); }} />
      </div>
    );
  }
  function ChipSingleInput({ field, value, onChange, disabled }) {
    const id = field.id;
    const opts = field.options || [];
    const vals = opts.map((o) => (typeof o === 'object' ? o.v : o));
    const custom = value && !vals.includes(value) ? String(value) : '';
    return (
      <div className="fe-chips">
        {opts.map((o) => {
          const v = typeof o === 'object' ? o.v : o;
          const l = typeof o === 'object' ? o.l : o;
          return (
            <button key={v} type="button" className={'fe-chip' + (value === v ? ' on' : '')} disabled={disabled} onClick={() => onChange(id, value === v ? '' : v)}>{l}</button>
          );
        })}
        {custom && <button type="button" className="fe-chip on fe-chip--custom" disabled={disabled} onClick={() => onChange(id, '')} title="Свой вариант — нажмите, чтобы убрать">{custom}<Ic n="x" s={12} /></button>}
        <AddOption disabled={disabled} onAdd={(t) => onChange(id, t)} />
      </div>
    );
  }

  function FieldInput({ field, value, onChange, disabled }) {
    const id = field.id;
    if (field.type === 'textarea') {
      return (
        <textarea
          className="fe-input fe-textarea"
          rows={3}
          value={value || ''}
          placeholder={field.placeholder || ''}
          disabled={disabled}
          onChange={(e) => onChange(id, e.target.value)}
        />
      );
    }
    if (field.type === 'select') {
      return (
        <select className="fe-input" value={value || ''} disabled={disabled} onChange={(e) => onChange(id, e.target.value)}>
          <option value="">—</option>
          {(field.options || []).map((o) => <option key={o} value={o}>{o}</option>)}
        </select>
      );
    }
    if (field.type === 'chips') {
      return <ChipsInput field={field} value={value} onChange={onChange} disabled={disabled} />;
    }
    if (field.type === 'chip_single') {
      return <ChipSingleInput field={field} value={value} onChange={onChange} disabled={disabled} />;
    }
    if (field.type === 'url_list') {
      const text = Array.isArray(value) ? value.join('\n') : (value || '');
      return (
        <textarea
          className="fe-input fe-textarea"
          rows={3}
          value={text}
          placeholder={field.placeholder || 'https://…'}
          disabled={disabled}
          onChange={(e) => onChange(id, e.target.value.split('\n').map((s) => s.trim()).filter(Boolean))}
        />
      );
    }
    if (field.type === 'checkbox') {
      return (
        <label className="fe-check">
          <input type="checkbox" checked={!!value} disabled={disabled} onChange={(e) => onChange(id, e.target.checked)} />
          <span>{field.label}</span>
        </label>
      );
    }
    if (field.type === 'media') {
      return <MediaFieldInput field={field} value={value} onChange={onChange} disabled={disabled} />;
    }
    if (field.type === 'composite') {
      const bag = value && typeof value === 'object' ? value : {};
      const layout = field.layout === 'stack' ? 'stack' : 'row';
      const patchSub = (subId, val) => onChange(id, { ...bag, [subId]: val });
      return (
        <div className={'fe-composite fe-composite--' + layout}>
          {(field.fields || []).map((sub) => (
            <div key={sub.id} className="fe-composite-cell">
              {sub.type !== 'checkbox' && (
                <span className="fe-composite-label">
                  {sub.label || 'Значение'}
                  {sub.unit ? <span className="fe-composite-unit">{sub.unit}</span> : null}
                </span>
              )}
              <FieldInput field={sub} value={bag[sub.id]} disabled={disabled} onChange={patchSub} />
            </div>
          ))}
        </div>
      );
    }
    return (
      <input
        className="fe-input"
        type={field.type === 'number' ? 'number' : 'text'}
        step={field.step}
        min={field.min}
        value={value == null ? '' : value}
        placeholder={field.placeholder || ''}
        disabled={disabled}
        onChange={(e) => onChange(id, field.type === 'number' ? e.target.value : e.target.value)}
      />
    );
  }

  /* просмотр картинок во весь экран: { images: [url], idx, title, sub } */
  function Lightbox({ lb, onClose, onIdx, actions }) {
    useEffect(() => {
      if (!lb) return undefined;
      const onKey = (e) => { if (e.key === 'Escape') onClose(); if (e.key === 'ArrowRight') onIdx((lb.idx + 1) % lb.images.length); if (e.key === 'ArrowLeft') onIdx((lb.idx - 1 + lb.images.length) % lb.images.length); };
      window.addEventListener('keydown', onKey); return () => window.removeEventListener('keydown', onKey);
    }, [lb, onClose, onIdx]);
    if (!lb || !lb.images.length) return null;
    const many = lb.images.length > 1;
    return (
      <div className="fe-lb" onClick={onClose} role="dialog" aria-label={lb.title || 'Просмотр'}>
        <button type="button" className="fe-lb-x" onClick={onClose} aria-label="Закрыть"><Ic n="x" s={18} /></button>
        {many && <button type="button" className="fe-lb-nav fe-lb-prev" onClick={(e) => { e.stopPropagation(); onIdx((lb.idx - 1 + lb.images.length) % lb.images.length); }} aria-label="Назад"><Ic n="chevron-left" s={22} /></button>}
        <figure className="fe-lb-fig" onClick={(e) => e.stopPropagation()}>
          <img src={lb.images[lb.idx]} alt={lb.title || ''} />
          <figcaption>
            <div><strong>{lb.title}</strong>{lb.sub && <span> · {lb.sub}</span>}{many && <span className="t-num"> · {lb.idx + 1}/{lb.images.length}</span>}</div>
            {actions}
          </figcaption>
        </figure>
        {many && <button type="button" className="fe-lb-nav fe-lb-next" onClick={(e) => { e.stopPropagation(); onIdx((lb.idx + 1) % lb.images.length); }} aria-label="Дальше"><Ic n="chevron-right" s={22} /></button>}
      </div>
    );
  }

  const VOTE_BTNS = [['like', 'thumbs-up', 'Нравится'], ['unsure', 'help-circle', 'Не уверен'], ['dislike', 'thumbs-down', 'Не нравится']];
  function StyleCardsSection({ section, votes, onChange, disabled, hideTitle }) {
    const Mock = window.DpBriefTemplateMock;
    const cardImages = (c) => (Mock && Mock.cardImages ? Mock.cardImages(c) : (Array.isArray(c.images) ? c.images.filter(Boolean) : []));
    const cardCover = (c) => (Mock && Mock.cardCoverUrl ? Mock.cardCoverUrl(c) : cardImages(c)[0] || null);
    const [lb, setLb] = useState(null);
    const setVote = (cardId, v) => {
      const next = { ...(votes || {}) };
      next[cardId] = next[cardId] === v ? null : v;
      onChange(section.id, next);
    };
    const lbCard = lb ? (section.cards || []).find((c) => c.id === lb.cardId) : null;
    return (
      <section className="fe-section">
        {!hideTitle && <h2 className="fe-section-title">{section.title}</h2>}
        {!hideTitle && section.subtitle && <p className="fe-section-sub">{section.subtitle}</p>}
        <div className="fe-style-grid">
          {(section.cards || []).map((c) => {
            const cover = cardCover(c);
            const imgs = cardImages(c);
            const extra = Math.max(0, imgs.length - 1);
            const vote = (votes || {})[c.id];
            return (
            <div key={c.id} className={'fe-style-card' + (vote ? ' is-' + vote : '')}>
              <button type="button" className="fe-style-thumb" onClick={() => imgs.length && setLb({ cardId: c.id, images: imgs, idx: 0, title: c.title, sub: c.sub })} aria-label={'Смотреть: ' + c.title}>
                {cover ? <img src={cover} alt="" loading="lazy" /> : <span className="fe-style-thumb-ph">📷</span>}
                {extra > 0 && <span className="fe-style-ref-badge">+{extra}</span>}
                {imgs.length > 0 && <span className="fe-style-zoom"><Ic n="maximize-2" s={13} /></span>}
              </button>
              <div className="fe-style-cap">
                <strong>{c.title}</strong>
                {c.sub && <span>{c.sub}</span>}
              </div>
              <div className="fe-style-votes">
                {VOTE_BTNS.map(([v, icon, title]) => (
                  <button key={v} type="button" className={'fe-vote fe-vote--' + v + (vote === v ? ' on' : '')} disabled={disabled} onClick={() => setVote(c.id, v)} aria-label={title} title={title}>
                    <Ic n={icon} s={15} />
                  </button>
                ))}
              </div>
            </div>
            );
          })}
        </div>
        <Lightbox
          lb={lb}
          onClose={() => setLb(null)}
          onIdx={(idx) => setLb((cur) => (cur ? { ...cur, idx } : cur))}
          actions={lbCard && !disabled && (
            <div className="fe-style-votes fe-lb-votes">
              {VOTE_BTNS.map(([v, icon, title]) => (
                <button key={v} type="button" className={'fe-vote fe-vote--' + v + ((votes || {})[lbCard.id] === v ? ' on' : '')} onClick={() => setVote(lbCard.id, v)} title={title}><Ic n={icon} s={15} /><span>{title}</span></button>
              ))}
            </div>
          )}
        />
      </section>
    );
  }

  const fieldVisible = (f, r) => { const M = window.DpBriefTemplateMock; return M && M.fieldVisible ? M.fieldVisible(f, r) : true; };
  const fieldOptions = (f, r) => { const M = window.DpBriefTemplateMock; return M && M.fieldOptions ? M.fieldOptions(f, r) : (f.options || []); };

  /* поля секции: showIf — условная видимость, optionsFrom — варианты по другому ответу; wide/textarea — на всю ширину сетки */
  function FieldsList({ fields, responses, disabled, onChange, upload }) {
    /* подгруппы (field.group): подряд идущие поля одной темы рисуются в общем контуре с подписью — ритм анкеты */
    const visibleFields = (fields || []).filter((f) => fieldVisible(f, responses));
    const chunks = [];
    visibleFields.forEach((f) => { const last = chunks[chunks.length - 1]; if (last && last.group === (f.group || null)) last.fields.push(f); else chunks.push({ group: f.group || null, fields: [f] }); });
    const renderField = (f) => {
          let field = f;
          if (f.optionsFrom) field = { ...field, options: fieldOptions(f, responses) };
          if (f.allowCustom) field = { ...field, customValue: responses[f.customId] };
          if (f.type === 'media' && upload) field = { ...field, uploadPid: upload.pid, uploadKind: upload.kind };
          const wide = f.wide || f.type === 'textarea' || f.type === 'url_list' || f.type === 'media' || f.type === 'composite';
          const fieldProps = { field, value: responses[f.id], disabled, onChange };
          if (f.type === 'checkbox') {
            return <div key={f.id} className={'fe-field' + (wide ? ' fe-field--wide' : '')}><FieldInput {...fieldProps} /></div>;
          }
          return (
            <label key={f.id} className={'fe-field' + (wide ? ' fe-field--wide' : '') + (f.type === 'chips' || f.type === 'chip_single' ? ' fe-field--chips' : '')}>
              <span className="fe-label">{f.label}{f.required ? ' *' : ''}</span>
              {f.hint && <span className="fe-hint">{f.hint}</span>}
              <FieldInput {...fieldProps} />
            </label>
          );
    };
    return (
      <div className="fe-fields">
        {chunks.map((ch, i) => (ch.group
          ? <section key={'g' + i} className="fe-group"><div className="fe-group-t">{ch.group}</div><div className="fe-group-b">{ch.fields.map(renderField)}</div></section>
          : ch.fields.map(renderField)))}
      </div>
    );
  }

  function FormSection({ section, responses, canEdit, onPatchTop, onPatchSection, hideTitle, upload }) {
    const sec = section;
    if (sec.kind === 'readonly_header') return <ReadonlyHeader section={sec} intake={{}} />;
    if (sec.kind === 'style_cards') {
      return <StyleCardsSection section={sec} votes={responses[sec.id]} disabled={!canEdit} onChange={onPatchSection} hideTitle={hideTitle} />;
    }
    if (sec.kind === 'room_group') {
      return <RoomGroupSection section={sec} items={responses[sec.id] || []} disabled={!canEdit} onChange={onPatchSection} hideTitle={hideTitle} />;
    }
    if (sec.kind === 'fields' || sec.kind === 'mini_survey') {
      return (
        <section className="fe-section">
          {!hideTitle && <h2 className="fe-section-title">{sec.title}</h2>}
          {!hideTitle && sec.hint && <p className="fe-section-sub">{sec.hint}</p>}
          <FieldsList fields={sec.fields} responses={responses} disabled={!canEdit} onChange={onPatchTop} upload={upload} />
        </section>
      );
    }
    return null;
  }

  function FormSchemaSections({ schema, responses, canEdit, onPatchTop, onPatchSection, upload }) {
    return (schema.sections || []).map((sec, i) => (
      <FormSection key={sec.id || sec.title + i} section={sec} responses={responses} canEdit={canEdit} onPatchTop={onPatchTop} onPatchSection={onPatchSection} upload={upload} />
    ));
  }

  function ReadonlyHeader({ section, intake }) {
    const val = (f) => {
      if (f.format === 'propertyType') return PT_LABELS[intake.propertyType] || PT_LABELS[intake.housingType] || intake.propertyType || '—';
      if (f.source === 'intake.address') return intake.address || '—';
      if (f.source === 'intake.clientName') return intake.clientName || '—';
      return '—';
    };
    return (
      <section className="fe-section">
        <h2 className="fe-section-title">{section.title || 'Объект'}</h2>
        <p className="fe-section-sub">Данные из профиля объекта · только просмотр</p>
        <div className="fe-readonly">
          {(section.fields || []).map((f) => (
            <div key={f.id}>
              <div className="fe-ro-label">{f.label}</div>
              <div className="fe-ro-val">{val(f)}</div>
            </div>
          ))}
        </div>
      </section>
    );
  }

  function RoomGroupSection({ section, items, onChange, disabled, hideTitle }) {
    const Mock = window.MeasureFieldTemplateMock;
    const patchItem = (idx, patch) => {
      const next = items.map((it, i) => (i === idx ? { ...it, ...patch } : it));
      onChange(section.id, next);
    };
    const addRoom = () => {
      const base = Mock ? Mock.defaultRoom(section) : { id: 'room-' + Date.now(), name: '' };
      onChange(section.id, [...items, base]);
    };
    const removeRoom = (idx) => {
      if (items.length <= (section.minItems || 1)) return;
      onChange(section.id, items.filter((_, i) => i !== idx));
    };
    const labelFor = (idx) => (section.labelTemplate || 'Комната {n}').replace('{n}', String(idx + 1));

    return (
      <section className="fe-section">
        {!hideTitle && <h2 className="fe-section-title">{section.title}</h2>}
        {!hideTitle && <p className="fe-section-sub">{section.hint || 'Повторяемые помещения · фото и размеры'}</p>}
        <div className="fe-room-list">
        {items.map((room, idx) => (
          <div key={room.id || idx} className="fe-room">
            <div className="fe-room-head">
              <span className="fe-room-title">{room.name || labelFor(idx)}</span>
              {!disabled && items.length > (section.minItems || 1) && (
                <button type="button" className="fe-chip" style={{ minHeight: 32, padding: '4px 10px' }} onClick={() => removeRoom(idx)}>Удалить</button>
              )}
            </div>
            <div className="fe-room-body">
              {(section.fields || []).map((f) => (
                f.type === 'checkbox' ? (
                  <FieldInput
                    key={f.id}
                    field={f}
                    value={room[f.id]}
                    disabled={disabled}
                    onChange={(fid, v) => patchItem(idx, { [fid]: v })}
                  />
                ) : (
                  <label key={f.id} className="fe-field">
                    <span className="fe-label">{f.label}{f.required ? ' *' : ''}</span>
                    <FieldInput
                      field={f}
                      value={room[f.id]}
                      disabled={disabled}
                      onChange={(fid, v) => patchItem(idx, { [fid]: v })}
                    />
                  </label>
                )
              ))}
            </div>
          </div>
        ))}
        </div>
        {!disabled && (
          <button type="button" className="fe-add-room" onClick={addRoom}>{section.addLabel || '+ Добавить помещение'}</button>
        )}
      </section>
    );
  }

  function FormEngine({
    schema,
    instance,
    intake,
    dealId,
    onInstanceChange,
    allowReopen,
  }) {
    const FI = window.FormInstances;
    const Mock = window.MeasureFieldTemplateMock;
    const [responses, setResponses] = useState(() => {
      if (instance.responses && Object.keys(instance.responses).length) return instance.responses;
      return Mock ? Mock.defaultResponses(schema) : {};
    });
    const [leftMs, setLeftMs] = useState(() => (FI ? FI.remainingMs(instance) : null));
    const saveTimer = useRef(null);

    const readonly = instance.accessState === 'submitted' || instance.accessState === 'expired'
      || (FI && FI.isMeasureExpired(instance));
    const canEdit = !readonly && (instance.accessState === 'active' || instance.accessState === 'in_progress');
    const pending = instance.accessState === 'pending';
    const timerActive = instance.accessState === 'active' && instance.activatedAt && !readonly;

    useEffect(() => {
      if (!FI) return undefined;
      const tick = () => {
        FI.checkMeasureExpiry(dealId);
        const fresh = FI.findInstance(dealId, (i) => i.id === instance.id);
        if (fresh && fresh.accessState !== instance.accessState) onInstanceChange(fresh);
        setLeftMs(FI.remainingMs(fresh || instance));
      };
      tick();
      const t = setInterval(tick, 15000);
      return () => clearInterval(t);
    }, [dealId, instance.id, instance.accessState, instance.activatedAt]);

    const persist = useCallback((nextResponses) => {
      if (!FI || readonly) return;
      FI.saveFieldResponses(dealId, instance.id, nextResponses, schema);
      const fresh = FI.findInstance(dealId, (i) => i.id === instance.id);
      if (fresh) onInstanceChange(fresh);
    }, [dealId, instance.id, schema, readonly, FI, onInstanceChange]);

    const patchTop = useCallback((id, val) => {
      setResponses((prev) => {
        const next = { ...prev, [id]: val };
        clearTimeout(saveTimer.current);
        saveTimer.current = setTimeout(() => persist(next), 400);
        return next;
      });
    }, [persist]);

    const patchSection = useCallback((sectionId, val) => {
      setResponses((prev) => {
        const next = { ...prev, [sectionId]: val };
        clearTimeout(saveTimer.current);
        saveTimer.current = setTimeout(() => persist(next), 400);
        return next;
      });
    }, [persist]);

    const progress = useMemo(() => (FI ? FI.computeSchemaProgress(schema, responses) : 0), [FI, schema, responses]);

    const onStart = () => {
      if (!FI) return;
      let nextResponses = responses;
      const roomSec = (schema.sections || []).find((s) => s.kind === 'room_group');
      const emptyRooms = roomSec && !(responses[roomSec.id] && responses[roomSec.id].length);
      if (!Object.keys(responses || {}).length || emptyRooms) {
        nextResponses = Mock ? Mock.defaultResponses(schema) : responses;
        FI.saveFieldResponses(dealId, instance.id, nextResponses, schema);
        setResponses(nextResponses);
      }
      const fresh = FI.activateMeasureField(dealId, instance.id);
      onInstanceChange(fresh);
    };

    const onDefer = () => {
      if (!FI) return;
      persist(responses);
      const fresh = FI.deferMeasureField(dealId, instance.id);
      onInstanceChange(fresh);
    };

    const onSubmit = () => {
      if (!FI) return;
      const fresh = FI.submitMeasureField(dealId, instance.id, responses, schema);
      if (fresh) onInstanceChange(fresh);
    };

    const onReopen = () => {
      if (!FI || !allowReopen) return;
      const fresh = FI.reopenMeasureField(dealId, instance.id);
      onInstanceChange(fresh);
    };

    return (
      <div className="fe-page">
        <header className="fe-head">
          <h1 className="fe-title">{schema.title || instance.title}</h1>
          <p className="fe-sub">{intake.address || 'Адрес не указан'} · {FI ? FI.stateLabel(instance.accessState) : instance.accessState}</p>
          <div style={{ display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap' }}>
            <Badge tone={instance.accessState === 'submitted' ? 'success' : instance.accessState === 'expired' ? 'warning' : 'info'} dot>
              {FI ? FI.stateLabel(instance.accessState) : instance.accessState}
            </Badge>
            <span className="fe-progress t-num">{progress}%</span>
            {timerActive && leftMs != null && (
              <span className={'fe-timer' + (leftMs <= 0 ? ' expired' : '')}>
                <Ic n="clock" s={14} />
                {leftMs <= 0 ? 'Время истекло' : <>Осталось {fmtTimer(leftMs)}</>}
              </span>
            )}
          </div>
        </header>

        {pending && (
          <div className="fe-banner">
            <Ic n="info" s={16} />
            <span>Заказ на замер сохранён. Нажмите «Начать заполнение» на объекте — откроется форма и запустится таймер 12 часов.</span>
          </div>
        )}

        {instance.accessState === 'in_progress' && (
          <div className="fe-banner ok">
            <Ic n="sunrise" s={16} />
            <span>Режим дозаполнения · таймер остановлен. Можно продолжить завтра или снова нажать «Начать заполнение» для нового 12-часового окна.</span>
          </div>
        )}

        {(instance.accessState === 'expired' || (FI && FI.isMeasureExpired(instance))) && (
          <div className="fe-banner warn">
            <Ic n="alert-triangle" s={16} />
            <span>Время заполнения истекло · форма только для просмотра.{allowReopen ? ' Менеджер может переоткрыть.' : ''}</span>
          </div>
        )}

        {instance.accessState === 'submitted' && (
          <div className="fe-banner ok">
            <Ic n="check-circle" s={16} />
            <span>Замер отправлен · данные переданы в SiteIntake{intake.requiresDesign ? ' · очередь ДП' : ' · очередь прораба'}.</span>
          </div>
        )}

        {(schema.sections || []).map((sec, i) => {
          if (sec.kind === 'readonly_header') return <ReadonlyHeader key={i} section={sec} intake={intake} />;
          if (sec.kind === 'style_cards') {
            return (
              <StyleCardsSection
                key={sec.id || i}
                section={sec}
                votes={responses[sec.id]}
                disabled={!canEdit}
                onChange={patchSection}
              />
            );
          }
          if (sec.kind === 'room_group') {
            return (
              <RoomGroupSection
                key={sec.id || i}
                section={sec}
                items={responses[sec.id] || []}
                disabled={!canEdit}
                onChange={patchSection}
              />
            );
          }
          if (sec.kind === 'fields' || sec.kind === 'mini_survey') {
            return (
              <section key={sec.title + i} className="fe-section">
                <h2 className="fe-section-title">{sec.title}</h2>
                {(sec.fields || []).map((f) => (
                  f.type === 'checkbox' ? (
                    <FieldInput key={f.id} field={f} value={responses[f.id]} disabled={!canEdit} onChange={patchTop} />
                  ) : (
                    <label key={f.id} className="fe-field">
                      <span className="fe-label">{f.label}{f.required ? ' *' : ''}</span>
                      <FieldInput
                        field={f.allowCustom ? { ...f, customValue: responses[f.customId] } : f}
                        value={responses[f.id]}
                        disabled={!canEdit}
                        onChange={patchTop}
                      />
                    </label>
                  )
                ))}
              </section>
            );
          }
          return null;
        })}

        <div className="fe-actions">
          {pending && (
            <Button variant="primary" size="md" iconLeft={<Ic n="play" s={16} />} onClick={onStart}>Начать заполнение</Button>
          )}
          {instance.accessState === 'in_progress' && (
            <Button variant="primary" size="md" iconLeft={<Ic n="play" s={16} />} onClick={onStart}>Начать заполнение</Button>
          )}
          {canEdit && instance.accessState === 'active' && (
            <>
              <Button variant="primary" size="md" iconLeft={<Ic n="send" s={16} />} onClick={onSubmit} disabled={progress < 80}>Отправить замер</Button>
              <Button variant="ghost" size="md" iconLeft={<Ic n="moon" s={16} />} onClick={onDefer}>Дозаполнить завтра</Button>
            </>
          )}
          {canEdit && instance.accessState === 'in_progress' && (
            <Button variant="primary" size="md" iconLeft={<Ic n="send" s={16} />} onClick={onSubmit} disabled={progress < 80}>Отправить замер</Button>
          )}
          {(instance.accessState === 'expired' || (FI && FI.isMeasureExpired(instance))) && allowReopen && (
            <Button variant="primary" size="md" iconLeft={<Ic n="rotate-ccw" s={16} />} onClick={onReopen}>Переоткрыть (менеджер)</Button>
          )}
        </div>
      </div>
    );
  }

  function FieldMeasureApp() {
    const { dealId, formToken, role, previewSchema } = parseQuery();
    const FI = window.FormInstances;
    const Mock = window.MeasureFieldTemplateMock;
    const [instance, setInstance] = useState(() => (FI && formToken ? FI.findByToken(dealId, formToken) : null));
    const intake = useMemo(() => (window.loadSiteIntake ? window.loadSiteIntake(dealId) : {
      address: '',
      propertyType: 'residential',
      clientName: '',
    }) || {}, [dealId, instance && instance.submittedAt, previewSchema]);

    useEffect(() => {
      const reload = () => {
        if (!FI || !formToken) return;
        setInstance(FI.findByToken(dealId, formToken));
      };
      window.addEventListener('remontpro:forms-updated', reload);
      return () => window.removeEventListener('remontpro:forms-updated', reload);
    }, [dealId, formToken]);

    if (previewSchema && Mock) {
      const schema = Mock.getSchema(previewSchema) || { sections: [], title: 'Шаблон' };
      const fakeInstance = {
        id: 'preview-' + previewSchema,
        kind: 'measure_field',
        templateId: previewSchema,
        title: schema.title,
        status: 'draft',
        linkToken: 'preview',
        responses: Mock.defaultResponses(schema),
        createdAt: new Date().toISOString(),
      };
      return (
        <FormEngine
          schema={schema}
          instance={fakeInstance}
          intake={intake}
          dealId={dealId}
          onInstanceChange={() => {}}
          allowReopen={false}
        />
      );
    }

    if (!formToken) {
      return (
        <div className="fe-empty">
          <Ic n="link-2-off" s={32} />
          <p>Нет токена формы. Откройте ссылку из задачи или CRM.</p>
        </div>
      );
    }
    if (!instance) {
      return (
        <div className="fe-empty">
          <Ic n="file-question" s={32} />
          <p>Форма не найдена для сделки {dealId}.</p>
          <p className="fe-sub">Сначала сохраните заказ на замер в CRM и materialize формы.</p>
        </div>
      );
    }
    if (instance.kind !== 'measure_field') {
      return (
        <div className="fe-empty">
          <Ic n="alert-circle" s={32} />
          <p>Эта ссылка не для замера на объекте.</p>
        </div>
      );
    }

    const schema = Mock ? Mock.getSchema(instance.templateId) : { sections: [], title: instance.title };

    return (
      <FormEngine
        schema={schema}
        instance={instance}
        intake={intake}
        dealId={dealId}
        onInstanceChange={setInstance}
        allowReopen={role === 'manager'}
      />
    );
  }

  window.FormEngine = FormEngine;
  window.FormSchemaSections = FormSchemaSections;
  window.FormSection = FormSection;
  window.FeLightbox = Lightbox;
  window.FieldMeasureApp = FieldMeasureApp;

  // страницы, где движок нужен только как библиотека (калькулятор, бриф клиента), ставят __FORM_ENGINE_EMBED_ONLY
  const root = document.getElementById('root');
  if (root && !window.__FORM_ENGINE_EMBED_ONLY) {
    ReactDOM.createRoot(root).render(<FieldMeasureApp />);
  }
})();
