// ferro-data.jsx — helpers, API layer, data mapping for Ferrolend КП

const RUB = (n) =>
  (Math.round(n) || 0).toLocaleString("ru-RU").replace(/ /g, " ") + " ₽";

const NUM = (n) => (Math.round(n) || 0).toLocaleString("ru-RU").replace(/ /g, " ");

const GLASS_NOTE = "Двери максимального остекления (ст/п. 24мм), сталь 1,5мм";

// ── Типы дверей, которые умеет считать прайс (для подсказки в выпадающем списке) ──
window.DOOR_TYPE_OPTIONS = [
  "Двери металлические распашные",
  "Двери металлические распашные противопожарные EI-60",
  "Двери металлические распашные противопожарные EIS-60",
  "Двери металлические распашные противопожарные EI-90",
  "Двери металлические распашные противопожарные EIS-90",
  "Двери решетчатые",
  "Двери с максимальным остеклением",
  "Двери с максимальным остеклением EIS-60",
  "Ворота распашные металлические",
  "Ворота распашные металлические противопожарные EI-60",
  "Люки металлические распашные",
  "Люки металлические распашные противопожарные EI-60",
  "Люки металлические распашные противопожарные EIS-60",
];

// ── Типы доводчика (для позиции-доводчика, см. pricing.split_closer_positions) ──
// «На обе створки» больше не отдельный вариант текста — это просто количество
// 2 у обычного «Доводчик рычажный» (см. extractor.detect_closer/доводчик_количество).
window.CLOSER_TYPE_OPTIONS = [
  "Доводчик рычажный",
  "Доводчик скользящий",
  "Notedo 025-095 со скользящей тягой",
  "Доводчик NOTEDO DC-120 серебро",
];

// ── Наличник: типовые ширины 20–100мм (шаг 10) + без наличника + обратный
// (автоматически при внутреннем открывании, см. extractor.detect_nalichnik) ──
window.NALICHNIK_OPTIONS = [
  "без наличника",
  "20мм", "30мм", "40мм", "50мм", "60мм", "70мм", "80мм", "90мм", "100мм",
  "обратный наличник",
];

// ── Открывание ──
window.OPENING_OPTIONS = ["левая", "правая", "?"];

// ── Зоны доставки: цена известна только "в пределах СПб", остальные — ручной ввод менеджером ──
window.DELIVERY_ZONES = [
  { label: "По СПб", cost: 5500 },
  { label: "До 10 км", cost: null },
  { label: "До 25 км", cost: null },
  { label: "До 50 км", cost: null },
  { label: "До 75 км", cost: null },
  { label: "До 100 км", cost: null },
];

// ── Что нового: список изменений для колокольчика в шапке, новые сверху, обновляется вручную при каждом деплое ──
window.CHANGELOG = [
  { date: "2026-08-20", text: "Расширил фикс \"Створки N/M\" на разные написания — раньше ловил только \"/\", теперь ещё дефис, длинное/короткое тире, \"х\"/\"x\"/\"×\", запятую, союз \"и\", с \":\"/\"мм\" между словом и числами или без (\"Створки: 900х300\", \"полотна 900мм/300мм\", \"створки 900 и 300\" и т.п.) — не только тот единственный формат, что был в разбираемой заявке" },
  { date: "2026-08-20", text: "Догон вчерашнего фикса \"Створки 900/300\": расчёт (раб. створка 900мм) уже был верным, но в попапе \"Текст заявки\" (вкладки \"Эта позиция\"/\"Вся заявка\") продолжал показываться СЫРОЙ вывод ИИ-нормализатора со старым \"Конструкция: 2-ств, равнопол.\" — таблица и попап противоречили друг другу. Теперь при срабатывании этого фикса текст в попапе тоже правится на \"неравнопол. (рабочая створка N мм)\", согласовано с расчётом" },
  { date: "2026-08-20", text: "Баг с деньгами/фурнитурой: заявка \"Створки 900/300\" (явно РАЗНЫЕ створки — рабочая 900мм, пассивная 300мм) — ИИ-нормализатор иногда ошибочно писал \"Конструкция: 2-ств, равнопол.\", хотя это прямо противоречит числам в тексте клиента. Обработчика для формулировки \"Створки N/M\" не было вообще (был только для симметричного случая \"ширина ОДНОЙ створки N\"). Теперь: если в исходном тексте клиента два РАЗНЫХ числа рядом со словом \"створки\" — результат берём напрямую из этих чисел (большее = рабочая створка), а не из вывода нормализатора" },
  { date: "2026-08-20", text: "Баг: подсказка у размера (проём/короб) показывала не дословную цитату из заявки, а свою же вычисленную строку вида \"2070×1250 мм (проём)\" — хотя клиент написал \"Размер проёма 1250/2070мм\". Теперь та ячейка, что реально найдена в тексте (не рассчитана по формуле от второй), цитирует заявку дословно; если рядом со словом \"проём\"/\"коробка\" пару чисел найти не удалось — честно оставляет прежнее вычисленное описание, а не гадает по первой попавшейся паре чисел в заявке (например размеру створок)" },
  { date: "2026-08-20", text: "Баг: RAL считался верно (клиент написал \"дверь белую\" → 9016), но подсказка показывала \"по умолчанию\" — цветовые слова (белый/серый/коричневый) искались только в мужском роде именительного падежа, а \"белую\"/\"белая\"/\"серая\" и т.п. не совпадали. Теперь ловятся все родовые/падежные окончания" },
  { date: "2026-08-20", text: "Баг: в колонке \"Раб. створка\" значение показывало \"равнопольная\", а подсказка рядом цитировала слово \"двухстворчатая\" — потому что за значение и за подсказку отвечали два разных места в коде с разными правилами, и второе (подсказка) не знало про формулировки вроде \"створки одинаковые по ширине\", только про буквальное слово \"равнопольная\". Теперь оба места используют одно и то же правило, вдобавок починил опечатку \"однаковые\" → \"одинаковые\", из-за которой часть таких формулировок вообще не срабатывала" },
  { date: "2026-08-20", text: "Добавлена подсказка \"откуда взято значение\" и на колонку \"Проект\" (марка/маркировка позиции, например Д-1) — раньше её там не было вообще" },
  { date: "2026-08-20", text: "Исправлены два бага новой подсказки: 1) подсказка улетала в левый верхний угол экрана вместо появления у курсора — причина в CSS-обёртке (display:contents ломает getBoundingClientRect), теперь позиция считается прямо от координат курсора; 2) блок примечания \"Наружная - сталь ...\" всегда показывал \"по умолчанию\", даже когда клиент явно написал \"наружная\" — потому что наружная трактовка технически решается через отдельный код-хинт, а не напрямую по слову клиента; теперь подсказка ищет в тексте клиента то самое слово, которое привело к уже посчитанному значению" },
  { date: "2026-08-20", text: "Подсказки \"откуда взято значение\" в КП переделаны: вместо нативной всплывающей подсказки браузера (голое слово без контекста) — своя подсказка с кусочком текста ДО и ПОСЛЕ найденной фразы (по 10 символов с каждой стороны), а сама фраза подсвечена жёлтым маркером — сразу видно, из какого места заявки взято значение. Работает везде: тип двери, размер, раб. створка, порог, RAL, наличник, сторона, замок, ручка, доводчик, каждый блок примечания" },
  { date: "2026-08-20", text: "На вкладке \"Оригинал\" в попапе \"Текст заявки\" можно отредактировать текст заявки прямо на месте: карандашик ✎ сверху справа — текст под ним становится редактируемым тут же, без отдельных попапов и блоков; галочка ✓ слева от него сохраняет и сразу пересчитывает заявку. Для случаев, когда заявка текстовая, а сверху приложен чертёж/фото с деталями, которых нет в тексте (например \"наружная, равнопольная\" у одной из 15 дверей) — правку нужно вписать ровно туда, где в тексте описана нужная позиция, не в конец" },
  { date: "2026-08-20", text: "Подсказки \"Из текста заявки\" на всех полях (порог, RAL/цвет, наличник, сторона, ручка, замок, доводчик, тип двери, раб. створка, блоки примечания) теперь берут цитату из ОРИГИНАЛЬНОГО текста клиента, а не из текста, переписанного ИИ-нормализатором перед расчётом — раньше подсказка могла показывать \"RAL 1015\", хотя клиент написал \"RAL - 1015\", и т.п. Расчётные значения не изменились, поменялся только текст цитаты в подсказке" },
  { date: "2026-08-20", text: "Исправлено: ручка снова ложно подменялась на \"систему антипаники\", когда в тексте (уже после нормализации) между словом \"замок\" и \"антипаника\" оказывалось длинное описание модели (\"Замок: цилиндр кл-верт, врезной противопожарный Apecs 2000-Panic-ZN с функцией антипаника\" — ~80 символов между ними, старая проверка смотрела только 20 символов вокруг). Теперь для лейблированного поля \"Замок: ...\" расстояние не ограничено — антипаника внутри поля замка не трогает ручку" },
  { date: "2026-08-20", text: "Попап «Текст заявки» раньше показывал только сырой текст (до ИИ-нормализации) — из-за этого не было видно, когда нормализация теряла детали при сжатии в поля. Теперь 3 вкладки: «Эта позиция»/«Вся заявка» показывают именно то, что реально ушло в расчёт (после нормализации), «Оригинал» — текст как прислал клиент, без изменений. Для фото-заявок отдельно осталась вкладка «Фото до OCR»" },
  { date: "2026-08-20", text: "Найдена и исправлена настоящая причина, почему порог/модель замка не подхватывались: ИИ-нормализатор (сжимает текст заявки в короткие поля перед расчётом) на нестандартных формулировках («минимальный порог», конкретная модель замка вроде Apecs 2000-Panic-ZN) иногда выбрасывает эти детали целиком из своего вывода — расчёт их просто не видел, хотя в исходном тексте заявки (та самая всплывающая подсказка) всё было на месте. Добавлены те же защитные подсказки-дублёры, что уже были для антипаники: если после нормализации порог/бренд замка пропал — подставляем обратно из исходного текста перед расчётом" },
  { date: "2026-08-20", text: "Исправлено: подсказка на размерах в КП врала — обе пары ячеек (проём и короб) показывали одни и те же числа короба, подписанные \"Из текста заявки\", даже когда в заявке был указан проём, а короб просто посчитан по формуле (−20/−40мм). Теперь у проёма и короба разные подсказки: найденная пара помечена как из текста, вторая — как рассчитанная" },
  { date: "2026-08-20", text: "Добавлено распознавание конкретной модели замка \"Apecs *-Panic-*\" (сертифицированный противопожарный врезной замок со встроенной антипаникой) — теперь берётся дословно из заявки вместо стандартного Fuaro/DL V1901, цилиндр (кл-вертушка/кл-кл) определяется как обычно" },
  { date: "2026-08-20", text: "Исправлено на реальной заявке: RAL с пробелами вокруг тире (\"RAL - 1015\") не распознавался и уходил в цвет по умолчанию \"стандарт\"; \"минимальный порог\" (без цифры) теперь распознаётся как 14мм; ручка больше не подменяется на \"систему антипаники\", если в тексте замок описан как \"с функцией антипаника с обычной ручкой\" (антипаника — функция механизма замка, а не отдельная система, заменяющая ручку)" },
  { date: "2026-08-20", text: "В примечании КП каждый кусочек (цинкогрунт, СКУД, эл-магн. замок, координатор, нержавейка, антипаника и т.д.) теперь показывает свою подсказку при наведении — откуда в тексте заявки он взят; если менеджер переписал примечание вручную, подсказки не показываются (текст уже не совпадает с автосборкой)" },
  { date: "2026-08-20", text: "В КП появилась подсказка при наведении на значение (тип двери, размер, раб. створка, порог, RAL/цвет, наличник, сторона открывания, замок, ручка, доводчик): показывает точный кусок текста заявки, откуда взято значение, или «по умолчанию — не указано», если это предположение системы, а не то, что явно написал клиент" },
  { date: "2026-08-20", text: "Исправлено: при системе антипаники замок и ручка в КП дублировали друг друга дословно одним и тем же текстом — теперь описываются по-разному (замок как замок, ручка как ручка), хотя это один физический комплект" },
  { date: "2026-08-18", text: "Загрузка файла в «Новом расчёте» — теперь одна вкладка «Файл» вместо «Таблица»/«PDF»/«Фото»: тип файла (таблица, PDF или фото) определяется автоматически, выбирать формат больше не нужно" },
  { date: "2026-08-18", text: "Карточка загрузки файла стала аккуратнее — можно перетащить файл в окно или нажать, чтобы выбрать; ссылка на Яндекс.Диск — в том же блоке" },
  { date: "2026-08-18", text: "PDF-заявки: подписи на эскизе двери (например, размер и положение остекления) теперь попадают в текст позиции, а не теряются молча" },
  { date: "2026-08-18", text: "В редакторе разметки фото можно вручную разрезать кусок на две позиции (двойной клик) или, наоборот, объединить два куска обратно в один (клик по линии между ними), а также сбросить разметку куска и начать заново" },
  { date: "2026-08-18", text: "Распознавание таблиц в фото/PDF заявок с тонкими линиями между строками стало точнее — раньше некоторые такие таблицы резались на строки неправильно" },
  { date: "2026-08-18", text: "Исправлено: упоминание слова «коробка»/«проём» не по поводу размера (например, «коробка в цвет двери») больше не сбивает автоопределение высоты 2100 мм по ГОСТ" },
  { date: "2026-08-18", text: "Сверка тендерной ведомости: добавлена AI-проверка спорных расхождений («не найдено»/«неоднозначно») — вторая пара глаз для случаев, которые не разрешились автоматически" },
  { date: "2026-08-17", text: "Дверь EI-90 теперь помечается уведомлением менеджеру «запросить стоимость двери у поставщика» и пометкой «Добровольная сертификация» первой строкой в примечании" },
  { date: "2026-08-17", text: "При внутреннем открывании двери автоматически ставится обратный наличник, а в примечании первой строкой добавляется «Дверь внутреннего открывания»" },
  { date: "2026-08-17", text: "Наценка за внутреннее открывание (+600/+800 ₽) теперь распознаётся и по формулировкам через тире («Открывание – вовнутрь»), и в обратном порядке слов («Направление открывания: внутреннее»)" },
  { date: "2026-08-12", text: "При загрузке фото заявки теперь можно выбрать файл из галереи или файлового менеджера — раньше сразу открывалась камера" },
  { date: "2026-08-11", text: "Загрузка фото/PDF/таблиц стала надёжнее при нестабильном интернете — при обрыве соединения система сама повторяет отправку" },
  { date: "2026-08-11", text: "Исправлено: доводчик в Excel-выгрузке КП иногда задваивался при пересчёте позиции, а такелаж и доставка не попадали в файл" },
  { date: "2026-08-11", text: "Исправлено: система антипаники (Panic/пушбар в описании фурнитуры) иногда не распознавалась при вводе текстом" },
  { date: "2026-08-10", text: "Распознавание шапки таблицы в фото/PDF заявок теперь работает с любыми формулировками заголовков столбцов, а не только с одним заранее известным форматом" },
  { date: "2026-08-10", text: "После распознавания фото — построчный экран проверки: видно каждую позицию с картинкой, найденный размер стекла округляется до 50 мм и показывается вместе с точностью" },
  { date: "2026-08-09", text: "Добавлено автоматическое определение примерного размера остекления по эскизу двери на фото" },
  { date: "2026-08-07", text: "Распознавание фото заявок стало заметно точнее — новый OCR-сервис лучше читает кириллицу и цифры размеров, чем раньше" },
  { date: "2026-08-07", text: "В редакторе разметки фото можно отметить строку с названиями столбцов таблицы («Шапка», 3-й клик по куску) — тогда код сам понимает, где марка, где обозначение, где количество и т.д., и точнее режет остальные строки" },
  { date: "2026-08-07", text: "Исправлено: высота 2100 мм без уточнения «по коробке»/«по проёму» теперь всегда считается размером проёма (по ГОСТ), как и задумано — раньше это иногда сбивалось на этапе автонормализации текста" },
  { date: "2026-08-06", text: "Форматы «Таблица» и «PDF» в «Новом расчёте»: можно загрузить сам файл (.xlsx/.csv или .pdf) вместо вставки текста руками" },
  { date: "2026-08-06", text: "Формат «Фото» в «Новом расчёте»: загрузка фото заявки (можно сразу несколько) — текст распознаётся автоматически, перед расчётом можно проверить и поправить" },
  { date: "2026-08-05", text: "Кнопка «Что нового» в шапке — здесь можно посмотреть последние изменения" },
  { date: "2026-08-05", text: "Добавлена подсказка «как посчитано стекло» — открывается у позиции рядом со стоимостью стекла" },
  { date: "2026-08-05", text: "Добавлен выбор зоны доставки при создании заявки (по умолчанию «По СПб» — 5 500 ₽)" },
  { date: "2026-07-06", text: "Исходный текст заявки сохраняется отдельно — вкладка «Оригинал» в попапе текста" },
  { date: "2026-07-03", text: "Улучшено распознавание размеров в табличных заявках (проём/коробка)" },
  { date: "2026-07-02", text: "Кнопка пересчёта запоминает выбор «с монтажом / без монтажа»" },
  { date: "2026-07-01", text: "Добавлена подсказка «как посчитана дверь» и наценки за сталь/цвет" },
  { date: "2026-07-01", text: "Добавлен переключатель ценовой группы (розница / от 5 / от 10 / от 50 / от 100 шт)" },
];

// ── Порог: ровно 4 значения умеет считать прайс ──
window.POROG_OPTIONS = ["14 мм", "45 мм", "без порога", "выпадающий порог"];

// ── Система антипаники: одно устройство на замок+ручку — выбор в одном поле зеркалится в другое ──
window.ANTIPANIC_VALUES = [
  "система антипаники Apecs на рабочую створку",
  "система антипаники DoorLock на рабочую створку (Италия)",
  "система антипаники DoorLock на обе створки (Италия)",
];

// ── Замок (из листа "замки" прайса) ──
window.ZAMOK_OPTIONS = [
  "замок Fuaro, цилиндр кл-кл",
  "замок Fuaro, цилиндр кл-верт",
  "замок Гардиан 12.01, цилиндр кл-кл",
  "замок Гардиан 12.01, цилиндр кл-верт",
  "замок DL V1901 с функц. антипаника",
  "Замок врезной противопожарный Apecs 2000-Panic-ZN , цилиндр кл-кл",
  "Замок врезной противопожарный Apecs 2000-Panic-ZN , цилиндр кл-вертушка",
  ...window.ANTIPANIC_VALUES,
];

// ── Ручка (из листа "ручки" прайса) ──
window.RUCHKA_OPTIONS = [
  "ручка черная нажимная",
  "ручка хром нажимная",
  "ручка-скоба приварная",
  "ручка-скоба хром 200мм",
  "ручка-скоба хром 300мм",
  "ручка-скоба DOORLOCK PHS11/32/2ст, L=450мм, нерж.",
  "ручка-скоба DOORLOCK PHS11/32/2ст, L=1000мм, нерж.",
  "ручка-скоба DOORLOCK PHS11/32/2ст, L=1500мм, нерж.",
  ...window.ANTIPANIC_VALUES,
];

// ── Полный список полей менеджерского расчёта ────────────────────────────────
const MGR_GROUPS = [
  {
    key: "izdelie",
    label: "Двери",
    fields: [
      { key: "doors",           label: "Двери" },
      { key: "framuga",         label: "Фрамуга" },
      { key: "naicenkaStal",    label: "Толщина стали" },
      { key: "naicenkaColor",   label: "Окрас" },
      { key: "dovodchikRab",    label: "Доводчик на раб.створку" },
      { key: "dovodchikPas",    label: "Доводчик на пас.створку" },
      { key: "glass",           label: "Стоимость стекла" },
      { key: "ramka",           label: "Рамка на стекло" },
      { key: "stopperGlassRab", label: "Отбойник на стекло раб.створки" },
      { key: "stopperGlassPas", label: "Отбойник на стекло пас.створки" },
      { key: "gratingRab",      label: "Решётка раб.створки" },
      { key: "gratingPas",      label: "Решётка доп.створки" },
      { key: "vypadPorog",      label: "Выпадающий порог" },
      { key: "stopperRab",      label: "Отбойная пластина раб.створка" },
      { key: "stopperPas",      label: "Отбойная пластина пас.створка" },
      { key: "lockMain",        label: "Замок основной" },
      { key: "lockExtra",       label: "Замок дополнительный" },
      { key: "cylinder",        label: "Цилиндр" },
      { key: "handlePress",     label: "Ручка нажимная" },
      { key: "handleBar",       label: "Ручка скоба" },
      { key: "coordinator",     label: "Координатор закрывания" },
      { key: "doorStopper",     label: "Дверной стопор" },
      { key: "antipanic",       label: "Антипаника раб.створка" },
      { key: "antipanic2",      label: "Антипаника пас.створка" },
      { key: "electromagnet",   label: "Электромагнитный замок" },
      { key: "herkon",          label: "Геркон" },
      { key: "electromech",     label: "Электромеханический замок" },
      { key: "cablePath",       label: "Кабельпереход" },
      { key: "cable",           label: "Кабель" },
      { key: "skud",            label: "Подготовка под СКУД" },
      { key: "openingDir",      label: "Внутреннее открывание (доплата)" },
    ],
  },
  {
    key: "montazh",
    label: "Монтаж",
    fields: [
      { key: "montDoor",        label: "Монтаж двери" },
      { key: "montCloserRab",   label: "Монтаж доводчика раб.створка" },
      { key: "montCloserPas",   label: "Монтаж доводчика пас.створка" },
      { key: "montCoordinator", label: "Монтаж координатора" },
      { key: "montPorog",       label: "Монтаж выпадающего порога" },
      { key: "montAntipanic",   label: "Монтаж антипаники раб.створка" },
      { key: "montAntipanic2",  label: "Монтаж антипаники пас.створка" },
      { key: "montStopperRab",  label: "Монтаж дверного стопора раб." },
      { key: "montStopperPas",  label: "Монтаж дверного стопора пас." },
      { key: "montPlateRab",    label: "Монтаж отбойной пластины раб." },
      { key: "montPlatePas",    label: "Монтаж отбойной пластины пас." },
      { key: "montGlassRab",    label: "Монтаж стекла раб.створка" },
      { key: "montGlassPas",    label: "Монтаж стекла пас.створка" },
      { key: "demontazh",       label: "Демонтаж" },
      { key: "takelDoor",       label: "Такелаж двери" },
      { key: "takelGlass",      label: "Такелаж стекла" },
    ],
  },
  {
    key: "uslugi",
    label: "Услуги",
    fields: [
      { key: "trash",            label: "Мусор" },
      { key: "electricity",      label: "Электроэнергия" },
      { key: "cleaning",         label: "Клининг" },
      { key: "stiz",             label: "Стиз" },
      { key: "packSingle",       label: "Упаковка однопольной двери" },
      { key: "packDouble",       label: "Упаковка двупольной двери" },
      { key: "furnitureReplace", label: "Замена фурнитуры+МДФ панелей" },
      { key: "handover",         label: "Сдача дольщикам" },
      { key: "deliveryMop",      label: "Доставка МОП" },
      { key: "deliveryApt",      label: "Доставка квартирные" },
      { key: "fines",            label: "Штрафы и непредвиденные" },
      { key: "genContractor",    label: "Ген.подрядные услуги" },
      { key: "retention",        label: "Гарантийное удержание" },
    ],
  },
];

// дефолтный объект mgr — все поля = 0
const MGR_DEFAULTS = Object.fromEntries(
  MGR_GROUPS.flatMap(g => g.fields.map(f => [f.key, 0]))
);

// canonical position shape used by all UI components
function mkPos(o) {
  return {
    id: o.id,
    proekt: o.proekt || "—",
    doorType: o.doorType || "—",
    proemH: o.proemH || "", proemW: o.proemW || "",
    korobH: o.korobH || "", korobW: o.korobW || "",
    qty: o.qty,
    rabStvorka: o.rabStvorka || "—",
    nalichnik: o.nalichnik || "—",
    opening: o.opening || "—",
    ral: o.ral || "—",
    porog: o.porog || "—",
    zamok: o.zamok || "—",
    ruchka: o.ruchka || "—",
    dovodchik: o.dovodchik !== false,
    dovodchikType: o.dovodchikType || "",
    dovodchikCount: o.dovodchikCount || 1,
    kind: o.kind || "door",
    note: o.note || "",
    noteAuto: o.noteAuto || "",
    noteBlocksDetail: o.noteBlocksDetail || [],
    positionText: o.positionText || "",
    positionTextNormalized: o.positionTextNormalized || "",
    alerts: o.alerts || [],
    fieldSources: o.fieldSources || {},
    glassNote: o.glassNote || GLASS_NOTE,
    priceHint: o.priceHint || "",
    glassHint: o.glassHint || "",
    lockHint: o.lockHint || "",
    glazingText: o.glazingText || "",
    glazingPriceOverride: o.glazingPriceOverride || null,
    mgr: Object.assign({}, MGR_DEFAULTS, o.mgr || {}),
  };
}

// derived totals for one position. installOn=false скрывает монтаж/услуги
// (переключатель "с монтажем/без монтажа" в КП) — не трогает сами mgr-поля,
// только сумму, поэтому включить обратно можно без потери цифр.
function calcPos(p, installOn = true) {
  const m = p.mgr;
  const qty = Math.max(1, +p.qty || 1);

  const itogoIzdelie = MGR_GROUPS[0].fields.reduce((s, f) => s + (+m[f.key] || 0), 0) * qty;
  const itogoMontazhFull = MGR_GROUPS[1].fields.reduce((s, f) => s + (+m[f.key] || 0), 0) * qty;
  const itogoUslugiFull  = MGR_GROUPS[2].fields.reduce((s, f) => s + (+m[f.key] || 0), 0) * qty;
  const demontazhFull    = (+m.demontazh || 0) * qty;
  const itogoMontazh = installOn ? itogoMontazhFull : 0;
  const itogoUslugi  = installOn ? itogoUslugiFull  : 0;
  const demontazh    = installOn ? demontazhFull    : 0;
  const montazhRaboty = itogoMontazh + itogoUslugi - demontazh;

  return {
    priceEach: MGR_GROUPS[0].fields.reduce((s, f) => s + (+m[f.key] || 0), 0),
    itogoIzdelie,
    itogoUslugi,
    itogoMontazh,
    demontazh,
    montazhRaboty,
    lineTotal: itogoIzdelie + itogoUslugi + itogoMontazh,
  };
}

function grandTotal(positions, installOn = true) {
  return positions.reduce((s, p) => s + calcPos(p, installOn).lineTotal, 0);
}

// ── API ──────────────────────────────────────────────────────────────────────

// Загрузка файла (фото/PDF/таблица) иногда падает с "Failed to fetch" — это
// сетевой обрыв на пути клиент→сервер (не ответ сервера с ошибкой, запрос до
// него физически не доехал), сервер это никак не залогирует. На реальных
// заявках такое уже случалось несколько раз подряд — вместо того чтобы
// заставлять менеджера каждый раз замечать ошибку и жать заново руками,
// повторяем один раз сами, только для именно этой сетевой ошибки (не для
// ответов сервера с кодом ошибки — те как были, так и остаются реальной
// ошибкой, не сетевым сбоем).
async function fetchWithRetry(url, options, retries = 1, delayMs = 1500) {
  for (let attempt = 0; ; attempt++) {
    try {
      return await fetch(url, options);
    } catch (e) {
      if (attempt >= retries) throw e;
      await new Promise((r) => setTimeout(r, delayMs));
    }
  }
}

// Файл режется на куски по ~500КБ и грузится по одному через /api/upload-chunk
// вместо одной большой передачи — fetch() (в отличие от скачивания файла
// браузером) не умеет докачивать после обрыва, поэтому один короткий сетевой
// сбой посреди загрузки крупного фото/PDF ронял всю передачу целиком. Кусками
// теряется максимум один кусок, и fetchWithRetry перезаливает только его.
const UPLOAD_CHUNK_SIZE = 512 * 1024;

function _uploadId() {
  if (window.crypto && window.crypto.randomUUID) return window.crypto.randomUUID();
  return Date.now().toString(36) + Math.random().toString(36).slice(2);
}

async function uploadFileChunked(file) {
  const uploadId = _uploadId();
  const total = Math.max(1, Math.ceil(file.size / UPLOAD_CHUNK_SIZE));
  let result = null;
  for (let i = 0; i < total; i++) {
    const start = i * UPLOAD_CHUNK_SIZE;
    const chunk = file.slice(start, start + UPLOAD_CHUNK_SIZE);
    const fd = new FormData();
    fd.append("upload_id", uploadId);
    fd.append("index", i);
    fd.append("total", total);
    fd.append("chunk", chunk, file.name);
    const r = await fetchWithRetry("/api/upload-chunk", { method: "POST", body: fd }, 3, 1500);
    if (!r.ok) throw new Error(await r.text());
    result = await r.json();
  }
  if (!result || result.status !== "done") throw new Error("Не удалось собрать файл на сервере");
  return result.token;
}

async function apiFetch(method, path, body) {
  const opts = { method, headers: { "Content-Type": "application/json" } };
  if (body !== undefined) opts.body = JSON.stringify(body);
  const r = await fetch("/api" + path, opts);
  if (!r.ok) throw new Error(await r.text());
  return r.json();
}

// ── ФОРМУЛА СТЕКЛА (размер + цена/м², редактируется в КП) ────────────────────
// Размер в тексте "остекление" всегда в порядке высота×ширина (см.
// extractor.detect_glazing / pricing.calc_glazing) — держим тот же порядок
// при чтении и записи, иначе площадь пересчитается неверно.
function parseGlazingDims(text) {
  const norm = (text || "").replace(/[xX×]/g, "х");
  const m = /(\d{3,4})х(\d{3,4})/.exec(norm);
  return m ? { h: m[1], w: m[2] } : { h: "", w: "" };
}

function withGlazingDims(text, h, w) {
  const t = text || "";
  const norm = t.replace(/[xX×]/g, "х");
  if (/\d{3,4}х\d{3,4}/.test(norm)) {
    return norm.replace(/\d{3,4}х\d{3,4}/, `${h}х${w}`);
  }
  return (t ? t + " " : "") + `${h}х${w}`;
}

// Цена/м² уже применённая наценке — парсим из готовой подсказки бэкенда
// (см. pricing.calc_glazing._hint), только для того чтобы предзаполнить поле
// ввода актуальным значением, если менеджер ещё не задавал ручную цену.
function parseGlazingRate(hint) {
  const m = /×\s*([\d\s]+)\s*₽\/м²/.exec(hint || "");
  return m ? m[1].replace(/\s/g, "") : "";
}

// ── NOTE BUILDER ─────────────────────────────────────────────────────────────

// [ПРАВИЛО] Тот же порядок блоков примечания, что и в Excel-КП (см.
// extractor.DEFAULT_NOTE_ORDER/kp_gen._build_note) — держать вручную в
// синхроне. Порядок настраивается на /corrections, фронт подтягивает
// сохранённое значение один раз при старте (меняется редко — только когда
// менеджер переставит блоки, свежего при перезагрузке страницы достаточно).
const DEFAULT_NOTE_ORDER = [
  "nerj", "aluminum_warning", "zincogrunt", "emag_lock", "skud", "coordinator",
  "steel", "glazing", "closer", "transom", "kickplate", "handle_antipanic",
];
window.NOTE_BLOCK_ORDER = null;
fetch("/api/note-block-order")
  .then((r) => r.json())
  .then((d) => { window.NOTE_BLOCK_ORDER = d.order; })
  .catch(() => {});

// [ПРАВИЛО] Собирает список кусочков примечания вместе с источником каждого
// (для покусочной подсказки в КП, см. NoteCell в ferro-app.jsx) — единственное
// место, где строится примечание; buildNoteFromBackend ниже просто склеивает
// текст этого же списка, чтобы не дублировать логику сборки в двух местах.
function buildNoteBlocksDetail(p, mode = "retail") {
  let fieldSrc = {};
  if (p.field_sources && typeof p.field_sources === "object") fieldSrc = p.field_sources;
  else if (typeof p.field_sources === "string") {
    try { fieldSrc = JSON.parse(p.field_sources) || {}; } catch (e) {}
  }
  const blockSrc = fieldSrc["примечание_блоки"] || {};

  const blocks = []; // [{id, text, source}]
  let rawBlocks = [];
  try { rawBlocks = JSON.parse(p.note_blocks || "[]"); } catch (e) {}
  for (const item of rawBlocks) {
    if (Array.isArray(item) && item.length === 2) {
      blocks.push({ id: item[0], text: item[1], source: blockSrc[item[0]] ?? null });
    }
  }
  if (p.steel && !/нержавеющ/i.test(p.steel)) {
    blocks.push({ id: "steel", text: p.steel.replace(/(\d)мм/g, "$1 мм") + ".", source: fieldSrc["сталь"] ?? null });
  }
  if (p.glazing) {
    let gl = p.glazing.charAt(0).toUpperCase() + p.glazing.slice(1);
    gl = gl.replace(/(\d{3,4})[×x](\d{3,4})(?!\s*мм)/g, "$1×$2мм");
    const is2 = (p.sash || "").includes("2 створч");
    if (is2 && !/створк|сторон/.test(gl)) gl += " в рабочей створке";
    blocks.push({ id: "glazing", text: gl + ".", source: null });
  }
  // [ПРАВИЛО] В рознице доводчик — отдельная позиция (pricing.split_closer_positions), в примечании
  // не повторяется. В тендерах остаётся внутри примечания, как было раньше.
  if (mode !== "retail" && p.closer && p.closer !== "без доводчика") {
    let cl = p.closer.charAt(0).toUpperCase() + p.closer.slice(1);
    const is2sash = (p.sash || "").includes("2 створч");
    if (is2sash && !/створк|на \w/.test(cl)) cl += " на активной створке";
    blocks.push({ id: "closer", text: cl + ".", source: fieldSrc["доводчик"] ?? null });
  }
  if (p.handle && /антипаник|пуш.?бар|push.?bar/i.test(p.handle)) {
    let hnd = p.handle.replace(/антипаника\s+DL\b/gi, "антипаники DoorLock");
    if (/\bDL\b|doorlock/i.test(p.handle) && !/\(италия\)/i.test(hnd)) hnd += " (Италия)";
    hnd = hnd.charAt(0).toUpperCase() + hnd.slice(1);
    blocks.push({ id: "handle_antipanic", text: hnd + ".", source: fieldSrc["ручка"] ?? null });
  }
  if (p.transom) blocks.push({ id: "transom", text: p.transom + ".", source: null });
  if (p.kickplate) blocks.push({ id: "kickplate", text: p.kickplate + ".", source: null });

  const order = window.NOTE_BLOCK_ORDER || DEFAULT_NOTE_ORDER;
  const rank = {};
  order.forEach((id, i) => { rank[id] = i; });
  blocks.sort((a, b) => (rank[a.id] ?? order.length) - (rank[b.id] ?? order.length));
  return blocks;
}

function buildNoteFromBackend(p, mode = "retail") {
  return buildNoteBlocksDetail(p, mode).map((b) => b.text).join(" ");
}

// ── DATA MAPPING: backend position → UI pos ──────────────────────────────────

function backendToPos(p, mode = "retail") {
  const qty = Math.max(1, +p.quantity || 1);

  const computed = {
    // Двери (цена за 1 шт.)
    doors:         Math.round(+p.price_door),
    framuga:       Math.round(+p.markup_transom),
    naicenkaStal:  Math.round(+p.markup_steel),
    naicenkaColor: Math.round(+p.markup_color),
    dovodchikRab:  Math.round(+p.markup_closer),
    glass:         Math.round(+p.markup_glazing),
    ramka:         Math.round(+p.markup_hardware + +p.markup_vent_grille),
    skud:          Math.round(+p.markup_skud || 0),
    coordinator:   Math.round(+p.markup_coordinator || 0),
    openingDir:    Math.round(+p.markup_opening_dir || 0),
    vypadPorog:    Math.round(+p.markup_threshold),
    lockMain:      Math.round(+p.markup_lock),
    handlePress:   Math.round(+p.markup_handle),
    // Монтаж (цена за 1 шт.)
    montDoor:      Math.round(+p.price_install),
    montCloserRab: Math.round(+p.install_closer),
    montCoordinator: Math.round(+p.install_coordinator || 0),
    montPorog:     Math.round(+p.install_threshold),
  };

  let overrides = {};
  if (p.mgr_json) {
    try { overrides = JSON.parse(p.mgr_json); } catch (e) {}
  }
  const mgr = Object.assign({}, MGR_DEFAULTS, computed, overrides);

  return mkPos({
    id: p.id,
    proekt: p.project_code || "—",
    doorType: p.type || "—",
    proemH: p.height_proem || "",
    proemW: p.width_proem || "",
    korobH: p.height || "",
    korobW: p.width || "",
    qty,
    rabStvorka: p.active_leaf_width ? String(p.active_leaf_width) : "—",
    nalichnik: (p.nalichnik && p.nalichnik !== "без наличника") ? p.nalichnik : "—",
    opening: p.side || "—",
    ral: p.color ? String(p.color).replace(/^ral[\s-]*/i, "") : "—",
    porog: (p.threshold && !/без порога/i.test(p.threshold)) ? p.threshold : "—",
    zamok: p.lock_ || "—",
    ruchka: p.handle || "—",
    dovodchik: !!(p.closer && p.closer !== "без доводчика"),
    dovodchikType: p.closer || "",
    dovodchikCount: p.dovodchik_count || 1,
    kind: p.kind || "door",
    note: p.note_override || buildNoteFromBackend(p, mode),
    noteAuto: buildNoteFromBackend(p, mode),
    noteBlocksDetail: buildNoteBlocksDetail(p, mode),
    positionText: p.position_text_raw || p.position_text || "",
    positionTextNormalized: p.position_text || "",
    alerts: Array.isArray(p.manager_alerts) ? p.manager_alerts : [],
    fieldSources: (() => {
      if (p.field_sources && typeof p.field_sources === "object") return p.field_sources;
      if (typeof p.field_sources === "string") {
        try { return JSON.parse(p.field_sources) || {}; } catch (e) { return {}; }
      }
      return {};
    })(),
    priceHint: p.price_hint || "",
    glassHint: p.markup_glazing_hint || "",
    lockHint: p.markup_lock_hint || "",
    glazingText: p.glazing || "",
    glazingPriceOverride: p.glazing_price_override || null,
    mgr,
  });
}

// ── MISC UTILS ────────────────────────────────────────────────────────────────

function fmtDate(iso) {
  if (!iso) return "";
  const d = new Date(iso);
  return d.toLocaleString("ru-RU", {
    day: "2-digit", month: "2-digit", year: "numeric",
    hour: "2-digit", minute: "2-digit",
  });
}

function statusLabel(s) {
  return { new: "Новая", processing: "Обрабатывается", ready: "Готово", sent: "Отправлено", error: "Ошибка" }[s] || s;
}

Object.assign(window, { RUB, NUM, GLASS_NOTE, MGR_GROUPS, MGR_DEFAULTS, mkPos, calcPos, grandTotal, apiFetch, backendToPos, buildNoteFromBackend, buildNoteBlocksDetail, fmtDate, statusLabel, parseGlazingDims, withGlazingDims, parseGlazingRate });
