// ========================================================================
// === FX-6: REQUESTS — буфер заявок на производство (v1.1) ===============
// Автономный data-файл. Читает BOM_GRANTA_L/PRODUCTS из data.jsx,
// сам data.jsx не правит. Интеграция в dashboard — в П8.
// ========================================================================

// Массив остаётся для совместимости: новая волна fx(reserve-v2-cleanup) убрала
// Буфер заявок с дашборда, но сама структура (REQUESTS, calcRequest) нужна,
// потому что launch_flow при прямом пуске добавляет сюда синтетическую
// запись `ORDER-XXX` со статусом 'launch' и резервирует W1.
const REQUESTS = [];

function calcRequest(product, qty, bufferFactor) {
  const buf = (typeof bufferFactor === 'number') ? bufferFactor : 0.10;
  // BOM по продукту заявки (а не хардкод Гранты) — иначе превью С-161/прочих
  // показывало детали Гранты. Фолбэк на эталон Гранты только для её семейства.
  const _sku = product && product.sku;
  let bom = (_sku && typeof window.getBomForSku === 'function') ? window.getBomForSku(_sku) : null;
  if (!bom || !bom.length) {
    bom = /^GRN|грант/i.test(String(_sku) + ' ' + String((product && product.name) || ''))
      ? (window.BOM_GRANTA_L || []) : [];
  }
  const n = Math.max(0, parseInt(qty, 10) || 0);

  const rows = bom.map(line => {
    const baseQty   = line.qty * n;
    const bufferQty = Math.ceil(baseQty * buf);
    const totalQty  = baseQty + bufferQty;
    const weight    = line.weight ? line.weight * totalQty : null;
    return {
      n:          line.n,
      name:       line.name,
      sku:        'BOM-' + String(line.n).padStart(3, '0'),
      material:   line.material || null,
      baseQty,
      bufferQty,
      totalQty,
      weightPerUnit: line.weight,
      weight,
    };
  });

  const totalWeightGrams = rows.reduce((s, r) => s + (r.weight || 0), 0);
  return { rows, totalWeightGrams, bufferFactor: buf, productSKU: product && product.sku, qty: n };
}

// ── ПРОГРЕСС НАРЯДА: накопитель отгрузки (Н1, спринт 2026-06-17) ────────────
// getRequestProgress(requestId) → { ordered, shipped, remaining, status }.
//   ordered  = req.qty (заявлено);
//   shipped  = Σ Shipment.qty (серверный SHIPMENTS + локальный optimistic);
//   remaining= ordered − shipped;
//   status   = 'done' | 'closed_early' (терминальные из req.status) |
//              'ready_to_close' (remaining<=0, ещё открыт) | 'in_progress'.
// Источник shipped — window._shippedTotalForRequest (data_materials), с фолбэком
// на прямое суммирование window.SHIPMENTS если хелпер ещё не загружен.
function getRequestProgress(requestId) {
  const empty = { ordered: 0, shipped: 0, remaining: 0, status: 'in_progress' };
  if (!requestId) return empty;
  const req = (window.REQUESTS || []).find(r => r && (r.id === requestId || r.externalId === requestId));
  const ordered = req ? (Number(req.qty) || 0) : 0;
  let shipped = 0;
  if (typeof window._shippedTotalForRequest === 'function') {
    shipped = window._shippedTotalForRequest(requestId) || 0;
  } else {
    for (const s of (Array.isArray(window.SHIPMENTS) ? window.SHIPMENTS : [])) {
      if (s && s.requestId === requestId) shipped += (Number(s.qty) || 0);
    }
  }
  const remaining = Math.max(0, ordered - shipped);
  // Терминальный статус наряда (после closeRequest) приоритетнее накопителя.
  const rs = req && req.status;
  let status;
  if (rs === 'done' || rs === 'closed_early') status = rs;
  else if (ordered > 0 && remaining <= 0) status = 'ready_to_close';
  else status = 'in_progress';
  return { ordered, shipped, remaining, status };
}

// closeRequest({ requestId, reason }) → Promise<{ status }>.
//   remaining>0 → reason ОБЯЗАТЕЛЕН (брак/веская причина): иначе reject.
//     → release остатка резерва (releaseFullSpec / releaseForRequest) +
//       POST /api/state/request/:id/close (AuditLog) → status 'closed_early'.
//   remaining<=0 → reason опц., status 'done' (резерв уже снят отгрузками).
// Оптимистично проставляем req.status локально (UI), серверный снапшот
// перезапишет. Backend пишет AuditLog (action close/close_early).
async function closeRequest(arg) {
  arg = arg || {};
  const requestId = arg.requestId;
  const reason = (arg.reason != null) ? String(arg.reason).trim() : '';
  if (!requestId) return Promise.reject(new Error('closeRequest: requestId обязателен'));
  const prog = getRequestProgress(requestId);
  const early = prog.remaining > 0;
  if (early && !reason) {
    return Promise.reject(new Error('Досрочное закрытие наряда требует причину'));
  }
  const status = early ? 'closed_early' : 'done';

  // Релиз остатка резерва. Идемпотентно (backend greedy-release удалит оставшиеся
  // строки резерва по requestId; нет строк — no-op). Делаем на ОБОИХ путях
  // (досрочно И нормально): нормальное закрытие после reload могло не получить
  // last-ship true-up (RESERVATIONS пуст) → подчищаем здесь (ревью-low 2026-06-17).
  {
    const releaseFn = window.releaseFullSpec || window.releaseForRequest;
    if (typeof releaseFn === 'function') { try { await releaseFn(requestId); } catch (_) {} }
  }

  // Оптимистично — статус заявки локально.
  const req = (window.REQUESTS || []).find(r => r && (r.id === requestId || r.externalId === requestId));
  if (req) req.status = status;
  try { window.dispatchEvent(new Event('reserve-changed')); } catch (_) {}

  // Backend: POST /api/state/request/:id/close { reason } → AuditLog + статус.
  if (window.API && typeof window.API.closeRequest === 'function') {
    try {
      await window.API.closeRequest(requestId, { reason: reason || undefined, early });
    } catch (_) { /* offline — статус остался локально, ретрай через очередь */ }
  }
  if (typeof window.logEvent === 'function') {
    window.logEvent('request.close', { requestId, status, early, reason: reason || null });
  }
  return { status };
}

Object.assign(window, { REQUESTS, calcRequest, getRequestProgress, closeRequest });
