/* calc2-wizard.jsx — пошаговая сборка сделки для менеджера (статус «Черновик»).
   9 этапов: 0 Клиент → 1 Объект → 2 Ремонт и пожелания → 3 Замер → 4 Дизайн-проект → 5 Состав работ →
   6 Материалы → 7 График → 8 Сводка и отправка. Следующий этап открывается только после выполнения условия
   предыдущего; пройденные можно открыть и править. Этапы 0–3 — формы calc2-wizard-forms.jsx, 4 — вкладка ДП карточки
   объекта, 5–8 — вкладки сметы (рендерит calc2-app через renderCalcTab). Позиция хранится в est.wizard.at.
   На этапе 5 при пустом составе автоматически применяется пресет + заготовка по типу объекта (objectBlueprint). */
(function () {
  const C = window.Calc2;
  const { Badge, Button } = window.DesignSystem_9e0a09;
  const { Ic } = window;
  const R = window.CalcIntakeReadiness;
  const F = window.Calc2WizardForms;
  const ASM = window.Calc2Assembly;
  const IR = window.Calc2IntakeRules;
  const { useState, useEffect, useMemo, useCallback } = React;

  const STAGES = [
    { n: 0, id: 'client', label: 'Клиент', short: 'Клиент', icon: 'user-round', kind: 'form', hint: 'Кто заказчик, как связаться и откуда пришёл' },
    { n: 1, id: 'object', label: 'Объект', short: 'Объект', icon: 'building-2', kind: 'form', hint: 'Адрес и доступ, параметры объекта — от них зависит заготовка сметы' },
    { n: 2, id: 'repair', label: 'Ремонт и пожелания', short: 'Ремонт', icon: 'paintbrush', kind: 'form', hint: 'Тип и уровень ремонта, стиль, технические решения, референсы, сроки, логистика' },
    { n: 3, id: 'measure', label: 'Замер', short: 'Замер', icon: 'ruler', kind: 'form', hint: 'Выезд, помещения с размерами и проёмами, инженерия, фото, план' },
    { n: 4, id: 'dp', label: 'Дизайн-проект', short: 'ДП', icon: 'pen-tool', kind: 'form', tab: 'dp', hint: 'Бриф → разработка у дизайнера → утверждённые чертежи и спецификация. По ним открываются «Работы» и «Материалы»' },
    { n: 5, id: 'works', label: 'Состав работ', short: 'Работы', icon: 'list-tree', kind: 'calc', tab: 'works', hint: 'Заготовка по объекту, пресет каталога, опции и дерево работ', confirm: { key: 'worksChecked', label: 'Состав работ проверен по объекту' } },
    { n: 6, id: 'mat', label: 'Материалы', short: 'Материалы', icon: 'package', kind: 'calc', tab: 'mat', hint: 'Черновые и чистовые материалы, количества и замены', confirm: { key: 'matChecked', label: 'Количества и замены материалов проверены' } },
    { n: 7, id: 'schedule', label: 'График', short: 'График', icon: 'calendar-range', kind: 'calc', tab: 'schedule', hint: 'Дата старта и этапы по датам' },
    { n: 8, id: 'summary', label: 'Сводка и отправка', short: 'Сводка', icon: 'send', kind: 'calc', tab: 'summary', hint: 'Итог для клиента, печать и отправка на согласование' },
  ];
  const BY_TAB = { client: 0, object: 1, repair: 2, measure: 3, dp: 4, works: 5, mat: 6, schedule: 7, summary: 8 };
  const has = (v) => v != null && v !== '';

  /* условие «Далее» для этапа: { ok, missing: [{ k: ключ поля, l: подпись }], note } — только обязательные поля */
  const M = (k, l) => ({ k, l });
  function gate(stage, ctx) {
    const it = ctx.intake || {};
    const est = ctx.est;
    let m = [];
    switch (stage.id) {
      case 'client': m = F.gateClient(it); break;
      case 'object': m = F.gateObject(it); break;
      case 'repair': m = F.gateRepair(it); break;
      case 'measure': m = F.gateMeasure(it); break;
      case 'dp': {
        if (!it.requiresDesign) return { ok: true, missing: [], note: 'Дизайн-проект не нужен — этап пропущен' };
        const dpSt = ((it.clientForms || {}).dpPrefs || {}).status;
        if (dpSt === 'submitted') m.push(M('dpBrief', 'бриф на дизайн-проект получен — нажмите «Согласовать бриф»'));
        else if (dpSt !== 'approved') m.push(M('dpBrief', 'бриф на дизайн-проект: заполнить с клиентом (или по ссылке), «Отправить» и «Согласовать»'));
        else if (!(it.team && it.team.designer && it.team.designer.name)) m.push(M('designer', 'выбрать дизайнера по брифу (в карточке ДП или «Команде» сделки)'));
        else if (!(est && est.status && est.status !== 'draft')) { // смета уже ушла клиенту (старые сделки) — не блокируем
          const D = window.DpPipeline; const pr = D && D.process && ctx.dealId ? D.process(ctx.dealId, it) : null;
          const WORD = { docs: 'чертежи', spec: 'спецификацию' };
          if (pr && !pr.works.ready) m.push(M('dpFinal', 'дизайнер ещё не утвердил ' + pr.works.missing.map((x) => WORD[x] || x).join(' и ') + ' (по ним считаем работы и материалы)'));
        }
        break;
      }
      case 'works': {
        const included = Object.values(est.nodes || {}).filter((n) => n && n.included).length;
        if (!included) m.push(M('works', 'пресет или хотя бы одна работа в составе'));
        try { const w = C.packageWarnings(est, ctx.catalog).filter((x) => x.level === 'error'); if (w.length) m.push(M('works', 'пакеты без фазы: ' + w.length)); } catch (e) {}
        break;
      }
      case 'mat': {
        try {
          const mm = C.materials(est, ctx.catalog);
          const zero = [...mm.rough, ...mm.finish].filter((r) => !(r.qty > 0));
          if (zero.length) m.push(M('mat', 'количество: ' + zero.slice(0, 3).map((r) => r.name).join(', ') + (zero.length > 3 ? ' +' + (zero.length - 3) : '')));
        } catch (e) { m.push(M('mat', 'материалы не рассчитались: ' + e.message)); }
        break;
      }
      case 'schedule':
        if (!has(est.schedule && est.schedule.start)) m.push(M('start', 'дата старта работ'));
        break;
      default: break;
    }
    if (stage.confirm && !(est.wizard && est.wizard[stage.confirm.key])) m.push(M('confirm', 'отметка «' + stage.confirm.label + '»'));
    return { ok: m.length === 0, missing: m };
  }

  /* новая карточка объекта: дефолты совпадают с тем, что подсвечено в чипах; заявка с сайта — из utm_<x>, name, phone в URL */
  function blankIntake() {
    const q = new URLSearchParams(window.location.search);
    const utm = {}; ['source', 'medium', 'campaign', 'term', 'content'].forEach((k) => { const v = q.get('utm_' + k); if (v) utm[k] = v; });
    const fromSite = Object.keys(utm).length > 0 || q.get('src') === 'site';
    const it = {
      created: true, status: 'new', requiresDesign: null, dpDecided: false, repairType: 'capital',
      clientName: q.get('name') || '', clientPhone: q.get('phone') || '', clientEmail: q.get('email') || '', address: q.get('address') || '',
      measurementDate: '', measuredBy: '',
      objectType: 'new_build', finishState: 'without', housingType: 'apartment', areaApprox: q.get('area') || '',
      leadSource: fromSite ? 'site' : '',
      prefs: { finishTier: '', style: [], tech: [], logistics: {}, deadlineNote: '', logisticsNote: '', freeText: '' },
      rooms: [], references: [], media: [], plans: [], brief: {}, clientForms: {}, access: {}, objectEng: {},
    };
    if (fromSite) it.lead = { source: 'site', utm, page: q.get('page') || '', message: q.get('message') || '', createdAt: new Date().toISOString() };
    return it;
  }

  function useIntake(dealId) {
    const load = () => (window.loadSiteIntake ? window.loadSiteIntake(dealId) : null);
    const [intake, setIntake] = useState(load);
    useEffect(() => {
      if (!load() && window.saveSiteIntake) { window.saveSiteIntake(dealId, blankIntake()); setIntake(load()); }
      // менеджер: кто создал заявку / отработал её — по режиму распределения (grab / roundrobin / manual)
      try { if (window.DealTeam && window.DealTeam.autoAssignManager) { const it = load(); if (it && !(it.team && it.team.manager)) { const p = window.DealTeam.autoAssignManager(dealId); if (p) setIntake(load()); } } } catch (e) {}
    }, [dealId]);
    useEffect(() => {
      const reload = () => setIntake(load());
      const evs = ['remontpro:intake-updated', 'remontpro:measure-submitted', 'remontpro:intake-route', 'storage'];
      evs.forEach((ev) => window.addEventListener(ev, reload));
      return () => evs.forEach((ev) => window.removeEventListener(ev, reload));
    }, [dealId]);
    const change = useCallback((next) => { if (window.saveSiteIntake) window.saveSiteIntake(dealId, next); setIntake(next); try { window.dispatchEvent(new Event('remontpro:intake-updated')); } catch (e) {} }, [dealId]);
    return [intake, change];
  }

  /* этап «График»: дата старта — единственное поле, которого нет в ScheduleView */
  function StartDateField({ est, setEst, miss }) {
    const v = (est.schedule && est.schedule.start) || '';
    return (
      <div className={'cc-wz-field' + (miss ? ' is-missing' : '')} data-wz-field="start">
        <label htmlFor="wz-start">Дата старта работ<span className="cc-wf-req">*</span></label>
        <input id="wz-start" type="date" className="cc-disc" value={v} onChange={(e) => setEst({ ...est, schedule: { ...(est.schedule || {}), start: e.target.value || null } })} />
        <span className="cc-wz-field-s">От неё считаются этапы графика; сдвинуть отдельный этап можно пином в таблице ниже.</span>
      </div>
    );
  }

  /* ─── заготовка состава работ: пресет по типу ремонта + включение/исключение этапов по типу объекта и техрешениям ─── */
  const presetFor = (intake) => F.presetFor(intake); // один выбор на анкету и мастер (calc2-wizard-forms.jsx)
  function leafIds(catalog, stageId) {
    const out = [];
    (catalog || []).forEach((ph) => (ph.stages || []).forEach((st) => {
      if (st.id !== stageId) return;
      (st.tasks || []).forEach((tk) => { if (tk.sub && tk.sub.length) tk.sub.forEach((su) => out.push(su.id)); else out.push(tk.id); });
    }));
    return out;
  }
  // запасной вариант без таблиц осей (calc2-template-axes.js не загружен): прежний objectBlueprint + заготовки по анкете
  function applyBlueprintLegacy(est, catalog, intake, intakeRules) {
    const bp = F.objectBlueprint(intake);
    let next = { ...est, nodes: { ...(est.nodes || {}) } };
    bp.exclude.forEach((sid) => leafIds(catalog, sid).forEach((id) => { next = C.setNodeIncluded(next, catalog, id, false, { skipSync: true }); }));
    bp.include.forEach((sid) => leafIds(catalog, sid).forEach((id) => { next = C.setNodeIncluded(next, catalog, id, true, { skipSync: true }); }));
    next = C.applyConditionalInclusion(next, catalog);
    return intakeRules ? intakeRules(next) : next;
  }
  // после пресета: тип объекта → состояние → техрешения → заготовки по анкете (Calc2Axes.applyAxes, таблицы — Конструктор).
  // Сборка общая с «Глазами клиента» в Конструкторе — lk/calc2-blueprint.js
  function applyBlueprint(est, catalog, intake, dealId) {
    const next = window.Calc2Blueprint.applyBlueprint(est, catalog, intake, dealId);
    if (next) return next;
    const intakeRules = IR ? (e) => IR.applyRules(e, catalog, intake, dealId).est : null; // этапы по ответам анкеты + смежные пакеты
    return applyBlueprintLegacy(est, catalog, intake, intakeRules);
  }
  /* после сборки мастером: в замере есть комнаты — «было / стало» разбивки открывается само (RoomsSyncBar в calc2-assembly-panel.jsx);
     разбивает менеджер по подтверждению (спека 2026-11-01-client-estimate-by-rooms §4) */
  function askRooms(intake) {
    const V = window.Calc2AssemblyVolumes;
    if (!V || !V.intakeRooms || !V.intakeRooms(intake || {}).length) return;
    setTimeout(() => { try { window.dispatchEvent(new Event('calc2:rooms-ask')); } catch (e) {} }, 0);
  }
  // подпись выбора мастера — по анкете без оси «Одна комната» (Calc2Blueprint.signature), без модуля — как раньше
  const bpSig = (intake, est) => { const BP = window.Calc2Blueprint, AX = window.Calc2Axes; return BP && BP.signature ? BP.signature(intake, est) : AX ? AX.signature(intake, est) : null; };
  const plural = (n, a, b, c) => { const m = n % 10, h = n % 100; return m === 1 && h !== 11 ? a : (m >= 2 && m <= 4 && (h < 10 || h >= 20)) ? b : c; };
  const AXIS_FIELD = { flex: '1 1 190px', minWidth: 0, margin: 0 };
  function BlueprintBar({ est, setEst, intake, catalog, dealId }) {
    const AX = window.Calc2Axes;
    const bp = F.objectBlueprint(intake, est);
    const preset = ASM && ASM.presetBySlug ? ASM.presetBySlug(presetFor(intake)) : null;
    const applied = est.wizard && est.wizard.blueprint;
    const run = () => {
      setEst((cur) => {
        try {
          if (ASM && ASM.saveSnapshot) ASM.saveSnapshot(cur); // снимок для «Отменить» (полоска заготовок по анкете ниже)
          let next = cur;
          if (ASM && preset) next = ASM.applyCatalogPreset(cur, preset.slug, 'replace', intake).estimate;
          next = applyBlueprint(next, catalog, intake, dealId); // состояние, техрешения и заготовки по анкете — внутри; комнаты и правки по комнатам сборка сохраняет
          return { ...next, wizard: { ...(next.wizard || {}), blueprint: { preset: preset ? preset.name : null, at: new Date().toISOString(), sig: AX ? bpSig(intake, next) : null } } };
        } catch (e) { alert('Не удалось применить заготовку: ' + e.message); return cur; }
      });
      askRooms(intake);
    };
    // оси шаблона: тип объекта и состояние. Выбор пишет только est.axes; состав меняет кнопка «Применить».
    // Ось «Одна комната» выведена (KSH-502): вместо неё — комнаты замера и фильтр комнат. Старая смета с осью показывает её только для чтения
    const axIntake = AX && AX.assemblyIntake ? AX.assemblyIntake(intake, est) : intake;
    const hasObject = !!(AX && AX.objectFor && AX.objectRules); // страница могла загрузить старый calc2-template-axes.js без оси
    const objects = hasObject ? AX.objectRules() : [];
    const intakeObject = hasObject ? AX.objectFor(intake, null) : null;
    const manualObject = hasObject && est.axes && est.axes.objectId && objects.some((r) => r.id === est.axes.objectId) ? est.axes.objectId : '';
    const pickObject = (v) => setEst((cur) => AX.chooseObject(cur, v));
    const states = AX ? AX.stateRules() : [];
    const sc = AX ? AX.scopeFor(axIntake, est) : { kind: 'object' };
    const intakeState = AX ? AX.stateFor(intake, null) : null;
    const manualState = est.axes && est.axes.stateId && states.some((r) => r.id === est.axes.stateId) ? est.axes.stateId : '';
    const pickState = (v) => setEst((cur) => AX.chooseState(cur, v));
    const sum = AX ? AX.summary(est, catalog, axIntake) : null;
    const changed = !!(AX && applied && applied.sig != null && applied.sig !== bpSig(intake, est));
    return (
      <div className={'cc-wz-bp' + (applied ? ' is-on' : '')}>
        <div className="cc-wz-bp-t"><Ic n={applied ? 'sparkles' : 'wand-2'} s={15} />{applied ? 'Заготовка применена' : 'Заготовка по объекту'}{preset ? ' · пресет «' + preset.name + '»' : ''}</div>
        {AX && (
          <div style={{ flex: '1 1 100%', display: 'flex', flexWrap: 'wrap', gap: '8px 14px' }}>
            <div className="cc-ir-field" style={AXIS_FIELD}>
              <span className="cc-ir-field-l">Тип ремонта</span>
              <div className="cc-ir-input" style={{ display: 'flex', alignItems: 'center' }}>{preset ? preset.name : '—'}</div>
            </div>
            {hasObject && objects.length > 0 && (
              <label className="cc-ir-field" style={AXIS_FIELD}>
                <span className="cc-ir-field-l">Тип объекта</span>
                <select className="cc-ir-input" value={manualObject} onChange={(e) => pickObject(e.target.value)}>
                  <option value="">{'Как в анкете — ' + (intakeObject ? intakeObject.name : 'без правил')}</option>
                  {objects.map((r) => <option key={r.id} value={r.id}>{r.name}</option>)}
                </select>
              </label>
            )}
            <label className="cc-ir-field" style={AXIS_FIELD}>
              <span className="cc-ir-field-l">Состояние</span>
              <select className="cc-ir-input" value={manualState} onChange={(e) => pickState(e.target.value)}>
                <option value="">{'Как в анкете — ' + (intakeState ? intakeState.name : 'без правил')}</option>
                {states.map((r) => <option key={r.id} value={r.id}>{r.name}</option>)}
              </select>
            </label>
            {sc.kind === 'room' && (
              <div className="cc-ir-field" style={AXIS_FIELD}>
                <span className="cc-ir-field-l">Объём (старая смета)</span>
                <div className="cc-ir-input" style={{ display: 'flex', alignItems: 'center' }}>{'Комната: ' + sc.room.name + ', ' + sc.area + ' м² — до разбивки по комнатам замера'}</div>
              </div>
            )}
          </div>
        )}
        <div className="cc-wz-bp-s">
          {sum && (applied || sum.works > 0) && <span><b>В смете: {sum.stages} {plural(sum.stages, 'этап', 'этапа', 'этапов')}, {sum.works} {plural(sum.works, 'работа', 'работы', 'работ')}.</b> </span>}
          {bp.notes.join(' ')}
          {sc.kind === 'room' && sc.room && sc.room.note && <span> {sc.room.note}</span>}
          {bp.include.length > 0 && <span> <b>Добавлено:</b> {bp.include.map(F.stageName).join(', ')}.</span>}
          {bp.exclude.length > 0 && <span> <b>Исключено:</b> {bp.exclude.map(F.stageName).join(', ')}.</span>}
          {changed && <span style={{ color: 'var(--warning-strong)', fontWeight: 700 }}> Выбор изменён — нажмите «Применить заново».</span>}
        </div>
        <button type="button" className="chip" onClick={run}><Ic n="rotate-ccw" s={12} />{applied ? 'Применить заново' : 'Применить'}</button>
      </div>
    );
  }

  /* полоска «заготовки по анкете» — этапы + смежные работы, включённые Calc2IntakeRules.applyRules сами при
     первом входе в пустой состав (или кнопкой «Применить» выше). Отмена восстанавливает снимок сметы, снятый
     до применения (ASM.saveSnapshot), и помечает intakeRulesApplied.undone — дальше само не применяется. */
  function IntakeRulesStrip({ est, setEst }) {
    const applied = est.intakeRulesApplied;
    if (!applied || applied.undone) return null;
    const labels = (applied.labels || []).filter(Boolean).join(', ');
    const undo = () => {
      if (!ASM) return;
      const restored = ASM.restoreSnapshot(est);
      const packages = { ...(restored.packages || {}) };
      (applied.packages || []).forEach((id) => { delete packages[id]; });
      setEst({ ...restored, packages, intakeRulesApplied: { ...applied, undone: true } });
    };
    const pl = (n, a, b, c) => (window.plural ? window.plural(n, a, b, c) : c);
    const nSt = applied.stagesCount || 0; const nRel = applied.relatedWorksCount || 0;
    return (
      <div className="cc-wz-bp is-on">
        <div className="cc-wz-bp-t"><Ic n="sparkles" s={15} />Из анкеты: {labels || '—'}</div>
        <div className="cc-wz-bp-s">→ {nSt} {pl(nSt, 'этап', 'этапа', 'этапов')} и {nRel} {pl(nRel, 'смежная работа', 'смежные работы', 'смежных работ')}</div>
        <button type="button" className="chip" onClick={undo}><Ic n="rotate-ccw" s={12} />Отменить</button>
      </div>
    );
  }

  function Calc2Wizard({ est, setEst, setEstQuiet, role, dealId, catalog, renderCalcTab, requestTab, onRequestHandled, total }) {
    const [intake, changeIntake] = useIntake(dealId);
    const patch = useCallback((p) => changeIntake({ ...(intake || {}), ...p }), [intake, changeIntake]);
    const dpMeta = useMemo(() => (R ? R.loadDesignProjectMeta(dealId) : null), [dealId, intake]);
    const readiness = useMemo(() => (R ? R.calcReadiness(intake, dpMeta) : { total: 0 }), [intake, dpMeta]);

    // синхронизация intake → est.project (площадь, тип ремонта, клиент) — «тихо»: сама по себе не создаёт смету в сторе
    useEffect(() => { if (intake && R) (setEstQuiet || setEst)((prev) => R.syncEstimateProject(prev, intake)); }, [intake]);

    // этапы дизайнера меняются в другом кабинете/вкладке — пересчитываем гейт ДП по событию пайплайна
    const [dpTick, setDpTick] = useState(0);
    useEffect(() => { const up = () => setDpTick((x) => x + 1); ['obitel:dp-pipeline', 'storage', 'obitel:sync'].forEach((e) => window.addEventListener(e, up)); return () => ['obitel:dp-pipeline', 'storage', 'obitel:sync'].forEach((e) => window.removeEventListener(e, up)); }, []);
    const ctx = { intake, est, catalog, dealId };
    const gates = useMemo(() => STAGES.map((s) => gate(s, ctx)), [intake, est, catalog, dealId, dpTick]);
    const reachable = STAGES.map((s, i) => gates.slice(0, i).every((g) => g.ok));
    const okChain = STAGES.map((s, i) => reachable[i] && gates[i].ok);
    const maxReach = reachable.lastIndexOf(true);
    const firstOpen = gates.findIndex((g) => !g.ok);
    const done = okChain.filter(Boolean).length;

    // позиция не прыгает сама при заполнении полей: только «Далее»/«Назад»/клик по этапу
    const stored = est.wizard && Number.isInteger(est.wizard.at) ? est.wizard.at : null;
    const [atState, setAt] = useState(() => (stored != null ? stored : (firstOpen < 0 ? 8 : firstOpen)));
    const [midOpen, setMidOpen] = useState(false); // телефон: панель «Чтобы продолжить» свёрнута в строку, тап раскрывает
    const at = Math.max(0, Math.min(atState, maxReach));
    // если этап закрылся (стёрли обязательное поле) — остаёмся на нём, а не прыгаем назад к сохранённой позиции после дозаполнения
    useEffect(() => { if (atState > maxReach) setAt(Math.max(0, maxReach)); }, [maxReach]);
    // телефон: текущий этап всегда в кадре полосы этапов
    useEffect(() => { try { const el = document.querySelector('.cc-wz-steps .cc-wz-step.is-cur'); if (el && el.scrollIntoView) el.scrollIntoView({ inline: 'center', block: 'nearest', behavior: 'smooth' }); } catch (e) {} }, [atState]);
    const goTo = (n) => { if (!reachable[n]) return; setAt(n); setEst((e) => ({ ...e, wizard: { ...(e.wizard || {}), at: n } })); try { const c = document.querySelector('.content'); if (c) c.scrollTop = 0; } catch (e) {} };

    useEffect(() => {
      if (!requestTab) return;
      const n = BY_TAB[requestTab];
      if (n != null) goTo(reachable[n] ? n : maxReach);
      onRequestHandled && onRequestHandled();
    }, [requestTab]);

    // первый заход на «Состав работ» с пустой сметой — применяем заготовку (пресет + этапы объекта) и
    // заготовки по анкете (intake rules) автоматически; intakeRulesApplied стоит и после «Отменить»
    // (с флагом .undone) — им же блокируем повторное само-применение.
    useEffect(() => {
      if (at !== 5 || !intake || !ASM) return;
      const included = Object.values(est.nodes || {}).filter((n) => n && n.included).length;
      if (included > 0 || (est.wizard && est.wizard.blueprint) || est.intakeRulesApplied) return;
      const preset = ASM.presetBySlug(presetFor(intake)); if (!preset) return;
      // от свежего состояния: другие автоэффекты шага (перенос спецификации ДП) могли уже обновить смету
      setEst((cur) => {
        try {
          if (Object.values(cur.nodes || {}).some((n) => n && n.included) || (cur.wizard && cur.wizard.blueprint) || cur.intakeRulesApplied) return cur;
          if (ASM.saveSnapshot) ASM.saveSnapshot(cur);
          let next = ASM.applyCatalogPreset(cur, preset.slug, 'replace', intake).estimate;
          next = applyBlueprint(next, catalog, intake, dealId); // состояние, техрешения и заготовки по анкете — внутри
          const AX = window.Calc2Axes;
          return { ...next, wizard: { ...(next.wizard || {}), blueprint: { preset: preset.name, at: new Date().toISOString(), sig: AX ? bpSig(intake, next) : null } } };
        } catch (e) { return cur; /* заготовка необязательна — менеджер выберет пресет сам */ }
      });
      askRooms(intake); // замер с комнатами — «было / стало» разбивки
    }, [at]);

    const stage = STAGES[at];
    const g = gates[at];
    const missingKeys = new Set(g.missing.map((x) => x.k));
    const jumpTo = (k) => {
      const el = document.querySelector('[data-wz-field="' + k + '"]'); if (!el) return;
      el.scrollIntoView({ behavior: 'smooth', block: 'center' });
      el.classList.add('is-flash'); setTimeout(() => el.classList.remove('is-flash'), 1600);
      const inp = el.querySelector('input, select, textarea'); if (inp && inp.type !== 'checkbox') { try { inp.focus({ preventScroll: true }); } catch (e) {} }
    };
    const next = at < 8 ? STAGES[at + 1] : null;
    const clientLabel = (intake && intake.clientName) || est.project.client || 'Новый клиент';
    const objectLabel = (intake && intake.address ? [intake.city, intake.address, intake.apt ? (intake.housingType === 'house' ? 'уч. ' : intake.housingType === 'commercial' ? 'пом. ' : 'кв. ') + intake.apt : ''].filter(Boolean).join(', ') : '') || (est.project.object !== 'Объект' ? est.project.object : '') || '';
    const FormCmp = stage.kind === 'form' ? { client: F.ClientStage, object: F.ObjectStage, repair: F.RepairStage, measure: F.MeasureStage, dp: F.DpStage }[stage.id] : null;
    // альбом ДП (страницы, статус, PDF) — из карточки объекта, когда дизайнер уже что-то загрузил
    const dpHasAlbum = stage.id === 'dp' && !!(dpMeta && dpMeta.pageCount);

    return (
      <div className="cc-wz">
        <div className="cc-wz-head">
          <div className="cc-wz-head-main">
            <div className="cc-wz-eyebrow">Сделка {dealId} · черновик сметы{intake && intake.lead ? ' · заявка с сайта' : ''}</div>
            <div className="cc-wz-title">{clientLabel}{objectLabel ? <span className="cc-wz-title-s"> · {objectLabel}</span> : null}</div>
          </div>
          <div className="cc-wz-head-kpi">
            <span className="cc-wz-kpi"><i>Этапов</i><b className="t-num">{done} / {STAGES.length}</b></span>
            <span className="cc-wz-kpi"><i>Данные объекта</i><b className="t-num">{readiness.total}%</b></span>
            <span className="cc-wz-kpi" title={at < 5 ? 'Появится после этапа «Состав работ»' : undefined}><i>Итого клиенту</i><b className="t-num">{at >= 5 ? C.rub(total) + ' ₽' : '—'}</b></span>
          </div>
        </div>

        <div className="cc-wz-progress" aria-hidden="true"><span style={{ width: Math.round(done / STAGES.length * 100) + '%' }} /></div>

        <ol className="cc-wz-steps" aria-label="Этапы сделки">
          {STAGES.map((s, i) => {
            const st = i === at ? 'cur' : okChain[i] ? 'done' : reachable[i] ? 'open' : 'locked';
            const block = gates.findIndex((gg, j) => j < i && !gg.ok);
            const prevLabel = block >= 0 ? STAGES[block].label + (gates[block].missing[0] ? ' — ' + gates[block].missing[0].l : '') : '';
            return (
              <li key={s.id} className={'cc-wz-step is-' + st}>
                <button type="button" disabled={!reachable[i]} onClick={() => goTo(i)}
                  title={!reachable[i] ? 'Закрыто. Сначала: ' + prevLabel : s.label}
                  aria-current={i === at ? 'step' : undefined}>
                  <span className="cc-wz-step-n">
                    {st === 'done' ? <Ic n="check" s={13} sw={3} /> : st === 'locked' ? <Ic n="lock" s={12} /> : <span className="t-num">{s.n}</span>}
                  </span>
                  <span className="cc-wz-step-l">{s.short}</span>
                </button>
              </li>
            );
          })}
        </ol>

        <div className="cc-wz-stage">
          <div className="cc-wz-stage-h">
            <span className="cc-wz-stage-ic"><Ic n={stage.icon} s={18} /></span>
            <div className="cc-wz-stage-t">
              <div className="cc-wz-stage-n">Этап {stage.n} из {STAGES.length - 1}</div>
              <h2>{stage.label}</h2>
              <div className="cc-wz-stage-s">{stage.hint}</div>
            </div>
            <div className="cc-wz-stage-badge"><Badge tone={g.ok ? 'success' : 'warning'} dot>{g.ok ? 'обязательное заполнено' : 'не хватает: ' + g.missing.length}</Badge><span className="cc-wz-req-legend"><span className="cc-wf-req">*</span> обязательно, остальное — по желанию</span></div>
          </div>

          {stage.kind === 'form' && intake && FormCmp && <FormCmp intake={intake} patch={patch} role={role} dealId={dealId} missing={missingKeys} goTo={goTo} />}
          {dpHasAlbum && intake && <window.SiteIntakeReadonlyPanel intake={intake} dealId={dealId} role={role} onChange={changeIntake} forceTab="dp" hideTabs />}
          {stage.kind === 'intake' && intake && (
            <div className={missingKeys.has('dpBrief') ? 'is-missing' : ''} data-wz-field="dpBrief"><window.SiteIntakeReadonlyPanel intake={intake} dealId={dealId} role={role} onChange={changeIntake} forceTab={stage.tab} section={stage.section} hideTabs /></div>
          )}
          {(stage.kind === 'form' || stage.kind === 'intake') && !intake && <div className="cc-wz-empty">Заводим карточку объекта…</div>}
          {stage.id === 'works' && intake && (
            <div data-wz-field="works" className={missingKeys.has('works') ? 'is-missing' : ''}>
              <IntakeRulesStrip est={est} setEst={setEst} />
              {window.Calc2CrossRulesPanel && <window.Calc2CrossRulesPanel est={est} setEst={setEst} catalog={catalog} intake={intake} dealId={dealId} role={role} />}
              <BlueprintBar est={est} setEst={setEst} intake={intake} catalog={catalog} dealId={dealId} />
            </div>
          )}
          {stage.id === 'schedule' && <StartDateField est={est} setEst={setEst} miss={missingKeys.has('start')} />}
          {stage.confirm && (
            <label className={'cc-wz-confirm' + (missingKeys.has('confirm') ? ' is-missing' : '')} data-wz-field="confirm">
              <input type="checkbox" checked={!!(est.wizard && est.wizard[stage.confirm.key])} onChange={(e) => setEst({ ...est, wizard: { ...(est.wizard || {}), [stage.confirm.key]: e.target.checked } })} />
              <span>{stage.confirm.label}</span>
            </label>
          )}
          {(stage.id === 'works' || stage.id === 'mat') && intake && F.DpWorksBridge && <F.DpWorksBridge intake={intake} dealId={dealId} total={total} tab={stage.id} est={est} setEst={setEst} catalog={catalog} />}
          {stage.kind === 'calc' && renderCalcTab(stage.tab)}
          {stage.id === 'dp' && intake && F.DpEngineeringStage && <F.DpEngineeringStage intake={intake} dealId={dealId} />}
          {intake && F.StageNote && <F.StageNote intake={intake} patch={patch} stageId={stage.id} />}
        </div>

        <div className="cc-wz-nav">
          <Button variant="ghost" size="md" disabled={at === 0} iconLeft={<Ic n="arrow-left" s={15} />} onClick={() => goTo(at - 1)}>Назад</Button>
          <div className={'cc-wz-nav-mid' + (midOpen ? ' is-open' : '')} onClick={() => setMidOpen((v) => !v)}>
            {g.ok
              ? <span className="cc-wz-ok"><Ic n="check-circle" s={14} />{g.note || (next ? 'Этап заполнен — можно к «' + next.label + '»' : 'Все этапы заполнены — отправьте смету клиенту')}</span>
              : <span className="cc-wz-miss"><Ic n="alert-circle" s={14} /><span>Чтобы продолжить:</span>{g.missing.map((x, i) => <button key={i} type="button" className="cc-wz-miss-chip" onClick={() => jumpTo(x.k)}>{x.l}</button>)}</span>}
          </div>
          {next
            ? <Button variant="accent" size="md" className="cc-wz-cta" disabled={!g.ok} onClick={() => goTo(at + 1)}>Далее: {next.short}<Ic n="arrow-right" s={15} /></Button>
            : <span className="cc-wz-nav-final">Кнопка «Отправить клиенту» — в сводке выше</span>}
        </div>
      </div>
    );
  }

  window.Calc2Wizard = Calc2Wizard;
  window.Calc2WizardStages = STAGES;
})();
