const db = require('../models');
const { sendResponse, sendError } = require('../utils/response');
const asyncHandler = require('../middlewares/asyncHandler');
const { getPagination, getPagingData } = require('../utils/pagination');
const { Op } = require('sequelize');
const routeCardService = require('../services/routeCardService');
const productionWipService = require('../services/productionWipService');
const PDFDocument = require('pdfkit');
const path = require('path');
const fs = require('fs');

const round3 = (value) => Number((parseFloat(value || 0) || 0).toFixed(3));

const parseOperationMaterialIds = (raw) => {
  if (!raw) return [];
  let parsed = raw;
  if (typeof parsed === 'string') {
    try {
      parsed = JSON.parse(parsed);
    } catch (e) {
      return [];
    }
  }
  if (!Array.isArray(parsed)) return [];
  const ids = [];
  parsed.forEach((entry) => {
    if (entry == null) return;
    if (Number.isFinite(parseInt(entry, 10))) {
      ids.push(parseInt(entry, 10));
      return;
    }
    if (typeof entry === 'object') {
      const candidate = entry.material_id || entry.materialId || entry.id || entry.value;
      if (Number.isFinite(parseInt(candidate, 10))) ids.push(parseInt(candidate, 10));
    }
  });
  return ids;
};

const parseOperationMaterialsWithQty = (raw) => {
  if (!raw) return [];
  let parsed = raw;
  if (typeof parsed === 'string') {
    try {
      parsed = JSON.parse(parsed);
    } catch (e) {
      return [];
    }
  }
  if (!Array.isArray(parsed)) return [];
  const rows = [];
  parsed.forEach((entry) => {
    if (entry == null) return;
    if (Number.isFinite(parseInt(entry, 10))) {
      rows.push({ material_id: parseInt(entry, 10), qty: null });
      return;
    }
    if (typeof entry === 'object') {
      const candidate = entry.material_id || entry.materialId || entry.id || entry.value;
      const materialId = parseInt(candidate, 10);
      if (!Number.isFinite(materialId)) return;
      const qtyRaw = entry.qty ?? entry.quantity;
      const qtyNum = qtyRaw == null || qtyRaw === '' ? null : parseFloat(qtyRaw);
      rows.push({
        material_id: materialId,
        qty: Number.isFinite(qtyNum) ? qtyNum : null
      });
    }
  });
  return rows;
};

const minutesBetween = (start, end) => {
  if (!start || !end) return 0;
  const s = new Date(start);
  const e = new Date(end);
  if (Number.isNaN(s.getTime()) || Number.isNaN(e.getTime())) return 0;
  const diffMs = e.getTime() - s.getTime();
  if (diffMs <= 0) return 0;
  return Math.round(diffMs / 60000);
};

/** BOM lines from fastener store — same rule as ProductionOrderForm (sheet lines list). */
const warehouseCodeSuggestsFastener = (code) => {
  const u = String(code || '').toUpperCase();
  return u.includes('FAST');
};

/** Posted material issues on PO, rolled up by material_id (same as production order API). */
const loadMaterialIssuedQtyByMaterialIdForPO = async (productionOrderId) => {
  if (!productionOrderId) return {};
  const postedIssuesForAgg = await db.MaterialIssue.findAll({
    where: { production_order_id: productionOrderId, status: 'POSTED' },
    attributes: ['id'],
    include: [
      {
        model: db.MaterialIssueItem,
        as: 'items',
        attributes: ['material_id', 'quantity'],
        required: false
      }
    ]
  });
  const map = {};
  for (const iss of postedIssuesForAgg) {
    for (const it of iss.items || []) {
      const mid = it.material_id;
      if (mid == null) continue;
      const k = String(mid);
      map[k] = round3((map[k] || 0) + round3(it.quantity));
    }
  }
  return map;
};

/**
 * All nesting → stores sheet lines for this PO (same scope as Production Order view raw materials).
 * Every NestingStoresRequest (newest first), each line with qty > 0; excludes fastener warehouse lines.
 */
const loadAllNestingStoresSheetLinesForRouteCard = async (productionOrderId) => {
  if (!productionOrderId) return [];
  const requests = await db.NestingStoresRequest.findAll({
    where: { production_order_id: productionOrderId },
    order: [['created_at', 'DESC']],
    attributes: ['id']
  });
  const out = [];
  for (const req of requests) {
    const lines = await db.NestingStoresRequestLine.findAll({
      where: { nesting_stores_request_id: req.id },
      attributes: [
        'id',
        'sheet_group_key',
        'material_id',
        'material_number',
        'nesting_confirmed_qty',
        'nesting_raw_material_text',
        'thickness',
        'grade_short',
        'max_length',
        'max_width',
        'dimension_unit',
        'warehouse_code'
      ],
      include: [
        { model: db.UOM, as: 'quantityUOM', attributes: ['uom_code'], required: false },
        {
          model: db.Material,
          as: 'material',
          attributes: ['id', 'material_number', 'description', 'length', 'width', 'thickness', 'base_uom_id'],
          required: false,
          include: [
            { model: db.UOM, as: 'baseUOM', attributes: ['uom_code'], required: false },
            { model: db.MaterialGrade, as: 'materialGrade', attributes: ['grade_code', 'grade_name'], required: false },
            { model: db.MaterialSheetType, as: 'materialSheetType', attributes: ['sheet_type_code', 'sheet_type_name'], required: false }
          ]
        }
      ],
      order: [['id', 'ASC']]
    });
    for (const ln of lines) {
      const j = typeof ln.toJSON === 'function' ? ln.toJSON() : ln;
      if (round3(j.nesting_confirmed_qty ?? 0) <= 0) continue;
      if (warehouseCodeSuggestsFastener(j.warehouse_code)) continue;
      out.push(j);
    }
  }
  return out;
};

/**
 * Same as productionNestingController.gradeShortFromMaterial — must match nesting sheet group_key / MRP key.
 */
const gradeShortFromMaterialForNesting = (m) => {
  if (!m) return '-';
  const sheetCode = String(m?.materialSheetType?.sheet_type_code || '').trim();
  const gradeCode = String(m?.materialGrade?.grade_code || '').trim();
  let gradeShort = '-';
  if (sheetCode && gradeCode) gradeShort = `${sheetCode} ${gradeCode}`;
  else if (gradeCode) gradeShort = gradeCode;
  else gradeShort = String(m?.materialGrade?.grade_name || '').trim() || '-';
  if (!gradeShort || gradeShort === '-') {
    gradeShort = String(m?.description || '').trim() || '-';
  }
  return gradeShort;
};

/**
 * Route card / PDF: material description from master. Pattern: thickness × description × width × length (all ×-separated).
 * Example: "1.3mm × HR × 1250 × 2500". Qty UOM is in its own column.
 */
const formatNestingRawMaterialDisplay = (thickness, dimUnit, width, length, materialDescription) => {
  const u = String(dimUnit || 'mm').trim() || 'mm';
  const t = round3(thickness);
  const w = round3(width);
  const l = round3(length);
  const thickWithUnit =
    u.toLowerCase() === 'mm' ? `${t}mm` : `${t} ${u}`;
  const desc = materialDescription != null ? String(materialDescription).trim() : '';
  const hasDesc = desc && desc !== '-';
  const dimPart = l > 0 || w > 0 ? `${w} × ${l}` : '';
  if (hasDesc && dimPart) return `${thickWithUnit} × ${desc} × ${dimPart}`;
  if (hasDesc) return `${thickWithUnit} × ${desc}`;
  if (dimPart) return `${thickWithUnit} ${dimPart}`;
  return thickWithUnit;
};

/** FT item codes (e.g. COMP001-FT-000055) are fasteners even when type is RAW_MATERIAL — aligns with ProductionWipStorageLedger.jsx */
const isFastenerForWipDisplay = (mat) => {
  if (!mat) return false;
  const t = String(mat.material_type || '').toUpperCase();
  if (t === 'CONSUMABLE' || t === 'PACKAGING') return true;
  const n = String(mat.material_number || '').toUpperCase();
  return /(?:^|[-_/])FT(?:[-_/]|$)/.test(n);
};

const wipMaterialCategoryLabel = (mat) => {
  if (isFastenerForWipDisplay(mat)) return 'Fastener';
  const t = String(mat?.material_type || '').toUpperCase();
  if (t === 'RAW_MATERIAL') return 'Raw material';
  return t ? t.replace(/_/g, ' ') : '—';
};

const buildWipMaterialDisplayString = (mat) => {
  if (!mat) return '';
  if (isFastenerForWipDisplay(mat)) {
    const d = String(mat.description || '').trim();
    return d || String(mat.material_number || '').trim() || '—';
  }
  const unit = String(mat.dimension_unit || 'mm').trim() || 'mm';
  const tv = mat.thickness != null && mat.thickness !== '' ? round3(mat.thickness) : 0;
  const wv = mat.width != null && mat.width !== '' ? round3(mat.width) : 0;
  const lv = mat.length != null && mat.length !== '' ? round3(mat.length) : 0;
  const desc = String(mat.description || '').trim();
  return formatNestingRawMaterialDisplay(tv, unit, wv, lv, desc || null);
};

const resolveGradeLabelForDisplay = (mat, lineGradeShort) => {
  if (mat != null) {
    const g = gradeShortFromMaterialForNesting(mat);
    if (g && g !== '-') return g;
  }
  const l = String(lineGradeShort || '').trim();
  if (l && l !== '-') return l;
  return null;
};

/**
 * Route card raw material = Production Order "nesting → stores sheet lines" only (not every nesting layout row).
 * Qty and issued match PO: required = nesting_confirmed_qty on the request line; issued = posted issues for that sheet material_id.
 */
const buildNestingSheetRowsByProductionOrder = async (productionOrderId) => {
  if (!productionOrderId) return [];
  const storeLines = await loadAllNestingStoresSheetLinesForRouteCard(productionOrderId);
  const issuedByMid = await loadMaterialIssuedQtyByMaterialIdForPO(productionOrderId);

  const rows = [];

  for (const line of storeLines) {
    const unit = String(line.dimension_unit || 'mm').trim() || 'mm';
    const t = round3(line.thickness);
    const len = round3(line.max_length);
    const wid = round3(line.max_width);
    const gradeLabel = String(line.grade_short || '').trim() || '-';
    const mid = parseInt(line.material_id, 10);
    const mat = line.material || null;
    const confirmed = round3(line.nesting_confirmed_qty ?? line.nesting_confirmed_quantity ?? 0);
    const midKey = Number.isFinite(mid) && mid > 0 ? String(mid) : '';
    const issued = midKey ? round3(issuedByMid[midKey] ?? issuedByMid[mid] ?? 0) : 0;
    const qtyUom =
      String(line.quantityUOM?.uom_code || mat?.baseUOM?.uom_code || '')
        .trim() || null;
    const gradeForDisplay = resolveGradeLabelForDisplay(mat, gradeLabel);
    const sgk = line.sheet_group_key != null ? String(line.sheet_group_key).trim() : '';
    const partFromGroup = /^\d+$/.test(sgk) ? parseInt(sgk, 10) : NaN;
    const sheetGroupPartMaterialId =
      Number.isFinite(partFromGroup) && partFromGroup > 0 ? partFromGroup : null;

    rows.push({
      key: `nsrl:${line.id}`,
      nesting_stores_request_line_id: line.id,
      material_id: Number.isFinite(mid) && mid > 0 ? mid : null,
      material_number: mat?.material_number || line.material_number || null,
      description: mat?.description || null,
      thickness: t,
      length: len,
      width: wid,
      dimension_unit: unit,
      quantity_uom_code: qtyUom,
      grade_short: gradeForDisplay || gradeLabel,
      stores_request_qty: confirmed,
      nesting_confirmed_qty: confirmed,
      issued_qty: issued,
      pending_to_issue_qty: round3(Math.max(confirmed - issued, 0)),
      nesting_raw_material_text:
        line.nesting_raw_material_text != null && String(line.nesting_raw_material_text).trim() !== ''
          ? String(line.nesting_raw_material_text).trim()
          : null,
      sheet_group_part_material_id: sheetGroupPartMaterialId
    });
  }

  if (rows.length === 0) {
    const nestingRows = await db.ProductionNesting.findAll({
      where: {
        production_order_id: productionOrderId,
        status: 'COMPLETED'
      },
      attributes: ['material_id', 'nesting_confirmed_quantity', 'thickness'],
      include: [
        {
          model: db.ProductionOrderComponent,
          as: 'productionOrderComponent',
          attributes: ['issued_quantity'],
          required: false
        },
        {
          model: db.Material,
          as: 'material',
          attributes: ['id', 'material_number', 'length', 'width', 'description', 'thickness', 'dimension_unit'],
          required: false,
          include: [
            { model: db.UOM, as: 'baseUOM', attributes: ['uom_code'], required: false },
            { model: db.MaterialGrade, as: 'materialGrade', attributes: ['grade_code', 'grade_name'], required: false },
            { model: db.MaterialSheetType, as: 'materialSheetType', attributes: ['sheet_type_code', 'sheet_type_name'], required: false }
          ]
        }
      ]
    });

    const byMat = new Map();
    for (const row of nestingRows) {
      const rj = typeof row.toJSON === 'function' ? row.toJSON() : row;
      const id = parseInt(rj.material_id, 10);
      if (!Number.isFinite(id) || id <= 0) continue;
      if (!byMat.has(id)) {
        byMat.set(id, {
          nesting_confirmed_qty: 0,
          component_issued: 0,
          sample: rj
        });
      }
      const b = byMat.get(id);
      b.nesting_confirmed_qty += round3(rj.nesting_confirmed_quantity || 0);
      b.component_issued += round3(rj.productionOrderComponent?.issued_quantity || 0);
    }

    for (const [mid, b] of byMat) {
      const qty = round3(b.nesting_confirmed_qty);
      if (qty <= 0) continue;
      const rj = b.sample;
      const mat = rj.material;
      const thickness = mat?.thickness != null && mat.thickness !== '' ? round3(mat.thickness) : round3(rj.thickness);
      const length =
        mat != null && mat.length != null && mat.length !== '' ? round3(mat.length) : null;
      const width =
        mat != null && mat.width != null && mat.width !== '' ? round3(mat.width) : null;
      const unit = String(mat?.dimension_unit || 'mm').trim() || 'mm';
      const gradeShort = gradeShortFromMaterialForNesting(mat);
      const midKey = String(mid);
      const issuedRollup = round3(issuedByMid[midKey] ?? issuedByMid[mid] ?? 0);
      const issuedUse = issuedRollup > 0 ? issuedRollup : round3(b.component_issued);

      rows.push({
        key: `nest-mat:${mid}`,
        nesting_stores_request_line_id: null,
        material_id: mid,
        material_number: mat?.material_number || null,
        description: mat?.description || null,
        thickness,
        length: length ?? 0,
        width: width ?? 0,
        dimension_unit: unit,
        quantity_uom_code: mat?.baseUOM?.uom_code ? String(mat.baseUOM.uom_code).trim() : null,
        grade_short: gradeShort,
        stores_request_qty: 0,
        nesting_confirmed_qty: qty,
        issued_qty: issuedUse,
        pending_to_issue_qty: round3(Math.max(qty - issuedUse, 0)),
        nesting_raw_material_text: null,
        sheet_group_part_material_id: null
      });
    }
  }

  const mids = [...new Set(rows.map((r) => r.material_id).filter((id) => id != null && id !== ''))];
  if (mids.length > 0) {
    const mats = await db.Material.findAll({
      where: { id: { [Op.in]: mids } },
      attributes: ['id', 'material_number', 'description'],
      include: [{ model: db.UOM, as: 'baseUOM', attributes: ['uom_code'], required: false }]
    });
    const mmap = new Map(mats.map((m) => [m.id, m]));
    rows.forEach((r) => {
      const m = mmap.get(r.material_id);
      if (m) {
        if (!r.material_number) r.material_number = m.material_number || null;
        if (!r.description) r.description = m.description || null;
        if (!r.quantity_uom_code && m.baseUOM?.uom_code) {
          r.quantity_uom_code = String(m.baseUOM.uom_code).trim() || null;
        }
      }
    });
  }

  rows.forEach((r) => {
    const pre =
      r.nesting_raw_material_text != null ? String(r.nesting_raw_material_text).trim() : '';
    const formatted = formatNestingRawMaterialDisplay(
      r.thickness,
      r.dimension_unit,
      r.width,
      r.length,
      r.description
    );
    // Nesting → stores lines: use the same thickness × description × W × L pattern as routing/WIP tables.
    // nesting_raw_material_text is MR snapshot ("MATNO | … grade …"); it reads as MRP in pickers.
    const fromNestingStoresLine = r.nesting_stores_request_line_id != null;
    if (fromNestingStoresLine) {
      r.raw_material = formatted && String(formatted).trim() !== '' ? formatted : pre;
    } else {
      r.raw_material = pre !== '' ? pre : formatted;
    }
  });

  rows.sort((a, b) => {
    if (a.nesting_stores_request_line_id != null && b.nesting_stores_request_line_id != null) {
      return Number(a.nesting_stores_request_line_id) - Number(b.nesting_stores_request_line_id);
    }
    if (a.nesting_stores_request_line_id != null) return -1;
    if (b.nesting_stores_request_line_id != null) return 1;
    return String(a.raw_material || '').localeCompare(String(b.raw_material || ''));
  });

  return rows;
};

/**
 * Fasteners allowed for WIP on a production order:
 * only materials that were sent to Stores via Material Request for that production order (MRP flow).
 */
const loadAllowedFastenerMaterialIdSetForProductionOrder = async (productionOrderId) => {
  const set = new Set();
  const poId = parseInt(productionOrderId, 10);
  if (!Number.isFinite(poId) || poId <= 0) return set;

  const reqs = await db.MaterialRequest.findAll({
    where: {
      production_order_id: poId,
      request_type: 'PRODUCTION_ORDER',
      status: { [Op.ne]: 'CANCELLED' },
    },
    attributes: ['id'],
    order: [['id', 'DESC']],
    limit: 50,
  });
  const reqIds = (reqs || []).map((r) => r.id).filter((id) => id != null);
  if (!reqIds.length) return set;

  const items = await db.MaterialRequestItem.findAll({
    where: { request_id: { [Op.in]: reqIds } },
    attributes: ['material_id'],
  });
  const mids = Array.from(
    new Set(
      (items || [])
        .map((it) => parseInt(it.material_id, 10))
        .filter((n) => Number.isFinite(n) && n > 0)
    )
  );
  if (!mids.length) return set;

  const mats = await db.Material.findAll({
    where: { id: { [Op.in]: mids } },
    attributes: ['id', 'material_number', 'description', 'material_type'],
  });
  (mats || []).forEach((m) => {
    if (isFastenerForWipDisplay(m)) set.add(m.id);
  });
  return set;
};

/**
 * Nesting → stores lines use sheet SKU on material_id and BOM/part id on sheet_group_key (see ProductionNestingView).
 * WIP and routing reference the part id, so we map display text by both keys.
 */
const buildNestingRawMaterialDisplayMaps = async (productionOrderId) => {
  const rawMaterialRows = await buildNestingSheetRowsByProductionOrder(productionOrderId);
  const nestingRawMaterialLineByMaterialId = new Map();
  const nestingRawMaterialLineByPartMaterialId = new Map();
  for (const line of rawMaterialRows) {
    const rm = line.raw_material != null ? String(line.raw_material).trim() : '';
    if (!rm) continue;
    const mid = parseInt(line.material_id, 10);
    if (Number.isFinite(mid) && mid > 0 && !nestingRawMaterialLineByMaterialId.has(mid)) {
      nestingRawMaterialLineByMaterialId.set(mid, rm);
    }
    const pid = line.sheet_group_part_material_id;
    if (pid != null && Number.isFinite(pid) && pid > 0) {
      const existing = nestingRawMaterialLineByPartMaterialId.get(pid);
      if (!existing) nestingRawMaterialLineByPartMaterialId.set(pid, rm);
      else if (existing !== rm) nestingRawMaterialLineByPartMaterialId.set(pid, `${existing} · ${rm}`);
    }
  }
  return { rawMaterialRows, nestingRawMaterialLineByMaterialId, nestingRawMaterialLineByPartMaterialId };
};

const resolveNestingDisplayForWipMaterial = (
  materialId,
  mat,
  nestingRawMaterialLineByPartMaterialId,
  nestingRawMaterialLineByMaterialId
) => {
  if (!mat || isFastenerForWipDisplay(mat)) return null;
  const mid = parseInt(materialId, 10);
  if (!Number.isFinite(mid) || mid <= 0) return null;
  return (
    nestingRawMaterialLineByPartMaterialId.get(mid) || nestingRawMaterialLineByMaterialId.get(mid) || null
  );
};

function parsePdfSignOffsJson(raw) {
  const empty = {
    verified_by_user_ids: [],
    accountant_user_ids: [],
    qc_incharge_user_ids: [],
  };
  if (raw == null || raw === '') return empty;
  let obj = raw;
  if (typeof raw === 'string') {
    try {
      obj = JSON.parse(raw);
    } catch {
      return empty;
    }
  }
  if (typeof obj !== 'object' || !obj) return empty;
  const ids = (a) =>
    Array.isArray(a) ? a.map((v) => parseInt(v, 10)).filter((n) => Number.isFinite(n) && n > 0) : [];
  const qcIncharge = ids(obj.qc_incharge_user_ids);
  const legacyQc = ids(obj.qc_user_ids);
  const legacyQa = ids(obj.qa_inspector_user_ids);
  const qc_incharge_user_ids =
    qcIncharge.length > 0 ? qcIncharge : [...new Set([...legacyQc, ...legacyQa])];
  return {
    verified_by_user_ids: ids(obj.verified_by_user_ids),
    accountant_user_ids: ids(obj.accountant_user_ids),
    qc_incharge_user_ids,
  };
}

async function formatUserNamesFromIds(userIds, companyId) {
  if (!userIds || userIds.length === 0) return '';
  const unique = [...new Set(userIds)];
  const where = { id: { [Op.in]: unique }, status: 'ACTIVE', is_active: true };
  if (companyId != null && !Number.isNaN(Number(companyId))) {
    where.company_id = Number(companyId);
  }
  const users = await db.User.findAll({
    where,
    attributes: ['id', 'username', 'first_name', 'last_name'],
  });
  const fmt = (u) => {
    const full = `${u.first_name || ''} ${u.last_name || ''}`.trim();
    return full || u.username || '';
  };
  return unique
    .map((id) => {
      const u = users.find((x) => String(x.id) === String(id));
      return u ? fmt(u) : '';
    })
    .filter(Boolean)
    .join(', ');
}

exports.getWipByOperation = asyncHandler(async (req, res) => {
  const {
    page = 1,
    limit = 200,
    companyId: queryCompanyId,
    plantId: queryPlantId,
    fiscalYearId,
    workCenterId,
    status,
    search,
    operationSearch,
    date
  } = req.query;

  const { limit: limitNum, offset } = getPagination(page, limit);
  const companyId = queryCompanyId || (req.user && !req.user.isSuperAdmin ? req.user.companyId : null);

  const opWhere = {};
  if (workCenterId) opWhere.work_center_id = parseInt(workCenterId, 10);
  if (status && String(status).trim().toUpperCase() !== 'ALL') {
    const statuses = String(status)
      .split(',')
      .map((s) => s.trim())
      .filter(Boolean);
    if (statuses.length > 0 && !statuses.map((s) => s.toUpperCase()).includes('ALL')) {
      opWhere.status = { [Op.in]: statuses };
    }
  }
  if (operationSearch) {
    opWhere[Op.or] = [
      { operation_code: { [Op.like]: `%${operationSearch}%` } },
      { operation_name: { [Op.like]: `%${operationSearch}%` } }
    ];
  }
  if (date) {
    const dayStart = new Date(`${date}T00:00:00.000Z`);
    const dayEnd = new Date(`${date}T23:59:59.999Z`);
    opWhere[Op.and] = [
      ...(opWhere[Op.and] || []),
      {
        [Op.or]: [
          { actual_start: { [Op.between]: [dayStart, dayEnd] } },
          { actual_end: { [Op.between]: [dayStart, dayEnd] } }
        ]
      }
    ];
  }

  const routeCardWhere = {};
  if (companyId) routeCardWhere.company_id = parseInt(companyId, 10);
  if (queryPlantId) routeCardWhere.plant_id = parseInt(queryPlantId, 10);
  if (fiscalYearId) routeCardWhere.fiscal_year_id = parseInt(fiscalYearId, 10);
  if (search) {
    routeCardWhere[Op.or] = [{ route_card_number: { [Op.like]: `%${search}%` } }];
  }

  const records = await db.RouteCardOperation.findAndCountAll({
    where: opWhere,
    include: [
      {
        model: db.RouteCard,
        as: 'routeCard',
        where: routeCardWhere,
        attributes: ['id', 'route_card_number', 'status', 'work_order_id', 'production_order_id'],
        include: [
          {
            model: db.WorkOrder,
            as: 'workOrder',
            attributes: ['id', 'work_order_number']
          },
          { model: db.ProductionOrder, as: 'productionOrder', attributes: ['id', 'production_order_number'] },
          { model: db.Material, as: 'finishedMaterial', attributes: ['id', 'material_number', 'description'] }
        ]
      },
      { model: db.WorkCenter, as: 'workCenter', attributes: ['id', 'work_center_code', 'description'] }
    ],
    limit: limitNum,
    offset,
    order: [
      [{ model: db.RouteCard, as: 'routeCard' }, 'id', 'DESC'],
      ['operation_sequence_no', 'ASC']
    ]
  });

  const response = getPagingData(records, page, limitNum);
  response.items = (response.items || []).map((op) => {
    const json = typeof op.toJSON === 'function' ? op.toJSON() : op;
    const routeCard = json.routeCard || {};
    const workOrder = routeCard.workOrder || {};
    const productionOrder = routeCard.productionOrder || {};
    const material = routeCard.finishedMaterial || {};
    const inputQty = parseFloat(json.input_qty || 0);
    const completedQty = parseFloat(json.completed_qty || 0);

    return {
      ...json,
      route_card_number: routeCard.route_card_number || '',
      work_order_number: workOrder.work_order_number || '',
      production_order_number: productionOrder.production_order_number || '',
      material_number: material.material_number || '',
      material_description: material.description || '',
      wip_qty: Math.max(inputQty - completedQty, 0)
    };
  });

  sendResponse(res, 200, true, 'WIP by operation retrieved successfully', response);
});

exports.getAll = asyncHandler(async (req, res) => {
  const { page = 1, limit = 25, search, status, companyId, plantId, fiscalYearId, workOrderId, routeCardNo } = req.query;
  const { limit: limitNum, offset } = getPagination(page, limit);

  const where = {};
  if (companyId) where.company_id = companyId;
  else if (req.user && !req.user.isSuperAdmin) where.company_id = req.user.companyId;
  if (plantId) where.plant_id = plantId;
  if (fiscalYearId) where.fiscal_year_id = fiscalYearId;
  if (workOrderId) where.work_order_id = workOrderId;

  if (status) where.status = status;
  if (search) {
    where[Op.and] = [...(where[Op.and] || []), { route_card_number: { [Op.like]: `%${search}%` } }];
  }
  if (routeCardNo) {
    const parentRows = await db.RouteCard.findAll({
      where: {
        route_card_number: { [Op.like]: `%${routeCardNo}%` },
        ...(where.company_id ? { company_id: where.company_id } : {}),
        ...(where.plant_id ? { plant_id: where.plant_id } : {}),
        ...(where.fiscal_year_id ? { fiscal_year_id: where.fiscal_year_id } : {})
      },
      attributes: ['id'],
      raw: true
    });
    const parentIds = parentRows.map((r) => r.id).filter(Boolean);
    where[Op.and] = [
      ...(where[Op.and] || []),
      {
        [Op.or]: [
          { route_card_number: { [Op.like]: `%${routeCardNo}%` } },
          ...(parentIds.length > 0 ? [{ previous_route_card_id: { [Op.in]: parentIds } }] : [])
        ]
      }
    ];
  }

  const records = await db.RouteCard.findAndCountAll({
    where,
    include: [
      {
        model: db.WorkOrder,
        as: 'workOrder',
        attributes: ['id', 'work_order_number', 'status']
      },
      { model: db.RouteCard, as: 'previousRouteCard', attributes: ['id', 'route_card_number'], required: false },
      { model: db.RouteCard, as: 'reworkRouteCards', attributes: ['id', 'route_card_number', 'status'], required: false },
      { model: db.ProductionFinalInspection, as: 'finalInspection', attributes: ['id', 'status'], required: false },
      { model: db.ProductionOrder, as: 'productionOrder', attributes: ['id', 'production_order_number'] },
      { model: db.Material, as: 'finishedMaterial', attributes: ['id', 'material_number', 'description'] },
      { model: db.BOMHeader, as: 'bom', attributes: ['id', 'bom_number', 'drawing_number', 'revision_number'] },
      { model: db.Company, as: 'company', attributes: ['id', 'code', 'name'] },
      { model: db.Plant, as: 'plant', attributes: ['id', 'code', 'name'] }
    ],
    limit: limitNum,
    offset,
    order: [['id', 'DESC']]
  });

  const response = getPagingData(records, page, limitNum);

  // Add operation status counts per route card for list display
  const ids = (response.items || []).map((r) => r.id).filter(Boolean);
  const countsByRcId = new Map();
  const qcPendingByRcId = new Map();
  const approvedFinalQcByWorkOrderId = new Map();
  if (ids.length > 0) {
    const rows = await db.RouteCardOperation.findAll({
      attributes: [
        'route_card_id',
        'status',
        [db.sequelize.fn('COUNT', db.sequelize.col('id')), 'count']
      ],
      where: { route_card_id: { [Op.in]: ids } },
      group: ['route_card_id', 'status']
    });

    rows.forEach((r) => {
      const rcId = r.get('route_card_id');
      const st = r.get('status');
      const count = parseInt(r.get('count'), 10) || 0;
      if (!countsByRcId.has(rcId)) countsByRcId.set(rcId, {});
      countsByRcId.get(rcId)[st] = count;
    });

    // QC pending = completed operations not yet QC approved.
    const opRows = await db.RouteCardOperation.findAll({
      where: { route_card_id: { [Op.in]: ids } },
      attributes: ['id', 'route_card_id', 'status'],
      include: [
        {
          model: db.ProductionQC,
          as: 'productionQC',
          attributes: ['id', 'status'],
          required: false
        }
      ]
    });
    opRows.forEach((op) => {
      const rcId = op.route_card_id;
      const st = String(op.status || '');
      if (st === 'COMPLETED' && String(op.productionQC?.status || '') !== 'APPROVED') {
        qcPendingByRcId.set(rcId, (qcPendingByRcId.get(rcId) || 0) + 1);
      }
    });

    // If any Route Card under the same Work Order has Final QC APPROVED,
    // align related rows to show APPROVED instead of lingering COMPLETED.
    const workOrderIds = Array.from(
      new Set(
        (response.items || [])
          .map((r) => parseInt(r?.work_order_id || r?.workOrder?.id, 10))
          .filter((v) => Number.isFinite(v) && v > 0)
      )
    );
    if (workOrderIds.length > 0) {
      const approvedRows = await db.ProductionFinalInspection.findAll({
        where: { status: 'APPROVED' },
        attributes: ['id'],
        include: [
          {
            model: db.RouteCard,
            as: 'routeCard',
            required: true,
            attributes: ['id', 'work_order_id'],
            where: { work_order_id: { [Op.in]: workOrderIds } }
          }
        ]
      });
      approvedRows.forEach((fi) => {
        const woId = parseInt(fi?.routeCard?.work_order_id, 10);
        if (Number.isFinite(woId) && woId > 0) approvedFinalQcByWorkOrderId.set(woId, true);
      });
    }
  }

  response.items = (response.items || []).map((r) => {
    const json = typeof r.toJSON === 'function' ? r.toJSON() : r;
    const counts = countsByRcId.get(json.id) || {};
    const totalOps = Object.values(counts).reduce((sum, v) => sum + (parseInt(v, 10) || 0), 0);
    const pending = counts.PENDING || 0;
    const inProgress = counts.IN_PROGRESS || 0;
    const completed = counts.COMPLETED || 0;
    const skipped = counts.SKIPPED || 0;
    const cancelled = counts.CANCELLED || 0;
    const terminalOps = completed + skipped + cancelled;
    const allOpsTerminal = totalOps > 0 ? terminalOps === totalOps : false;
    const qcPendingCount = qcPendingByRcId.get(json.id) || 0;
    const reworkCards = Array.isArray(json.reworkRouteCards) ? json.reworkRouteCards : [];
    const reworkTotal = reworkCards.length;
    const reworkOpen = reworkCards.filter((rc) => rc?.status === 'OPEN').length;
    const reworkClosed = reworkCards.filter((rc) => rc?.status === 'CLOSED').length;
    const productionClosureReady = allOpsTerminal && qcPendingCount === 0 && reworkOpen === 0;
    const isReferenceRow = Boolean(json?.previous_route_card_id);
    const isFinalQcReworkEligible = isReferenceRow && !Boolean(json?.source_production_qc_id);
    // Keep this readiness rule aligned with productionFinalInspectionController.checkRouteCardReadyForFinalQC.
    const finalQcReady = totalOps > 0
      && pending === 0
      && inProgress === 0
      && qcPendingCount === 0
      && reworkOpen === 0;
    let finalQcStatus = json?.finalInspection?.status || null;
    // Show an actionable status even before Final QC record is created:
    // - PENDING when ready to move
    // - IN_PROGRESS while route card operations are still in progress
    // For reference rows, apply this only for Final QC rework-eligible cards.
    if (!finalQcStatus && (!isReferenceRow || isFinalQcReworkEligible)) {
      if (finalQcReady) finalQcStatus = 'PENDING';
      else if (inProgress > 0) finalQcStatus = 'IN_PROGRESS';
      else if (pending > 0 || qcPendingCount > 0 || reworkOpen > 0) finalQcStatus = 'PENDING';
    }
    const woId = parseInt(json?.work_order_id || json?.workOrder?.id, 10);
    const hasApprovedInSameWO = Number.isFinite(woId) && approvedFinalQcByWorkOrderId.get(woId) === true;
    if (hasApprovedInSameWO && finalQcStatus === 'COMPLETED') {
      finalQcStatus = 'APPROVED';
    }
    return {
      ...json,
      operation_status_counts: { ...counts, TOTAL: totalOps },
      operation_status_summary: `P:${pending} IP:${inProgress} C:${completed} / ${totalOps}`,
      rework_summary: {
        total: reworkTotal,
        open: reworkOpen,
        closed: reworkClosed
      },
      qc_pending_count: qcPendingCount,
      production_closure_ready: productionClosureReady,
      final_qc_ready: finalQcReady,
      final_inspection_exists: Boolean(json?.finalInspection?.id),
      final_qc_status: finalQcStatus,
      final_qc_rework_eligible: isFinalQcReworkEligible
    };
  });

  sendResponse(res, 200, true, 'Route Cards retrieved successfully', response);
});

exports.getById = asyncHandler(async (req, res) => {
  const { id } = req.params;
  const record = await db.RouteCard.findByPk(id, {
    include: [
      { model: db.WorkOrder, as: 'workOrder', attributes: ['id', 'work_order_number', 'status', 'work_order_quantity', 'routing_id'] },
      { model: db.RouteCard, as: 'previousRouteCard', attributes: ['id', 'route_card_number'], required: false },
      { model: db.RouteCard, as: 'reworkRouteCards', attributes: ['id', 'route_card_number', 'status'], required: false },
      { model: db.ProductionFinalInspection, as: 'finalInspection', attributes: ['id', 'status'], required: false },
      {
        model: db.ProductionOrder,
        as: 'productionOrder',
        attributes: ['id', 'production_order_number', 'order_quantity'],
        include: [
          {
            model: db.CustomerPO,
            as: 'customerPO',
            attributes: ['id', 'po_number'],
            required: false,
            include: [
              {
                model: db.BusinessPartner,
                as: 'customer',
                attributes: ['id', 'bp_number', 'name'],
                required: false
              }
            ]
          },
          {
            model: db.ProductionOrderComponent,
            as: 'components',
            attributes: ['id', 'material_id', 'required_quantity'],
            required: false
          }
        ]
      },
      { model: db.Material, as: 'finishedMaterial', attributes: ['id', 'material_number', 'description', 'drawing_number', 'revision_number'] },
      { model: db.BOMHeader, as: 'bom', attributes: ['id', 'bom_number', 'drawing_number', 'revision_number'] },
      {
        model: db.RouteCardOperation,
        as: 'operations',
        include: [
          { model: db.ProductionOperator, as: 'operator', attributes: ['id', 'operator_name', 'mobile_no', 'shift'], required: false },
          { model: db.Department, as: 'department', attributes: ['id', 'department_code', 'department_name'] },
          { model: db.WorkCenter, as: 'workCenter', attributes: ['id', 'work_center_code', 'description'] },
          {
            model: db.ProductionQC,
            as: 'productionQC',
            attributes: ['id', 'production_qc_number', 'status', 'checked_qty', 'rework_qty', 'moved_to_qc_at'],
            required: false,
            include: [
              { model: db.RouteCard, as: 'reworkRouteCard', attributes: ['id', 'route_card_number', 'status'], required: false }
            ]
          }
        ]
      }
    ],
    order: [[{ model: db.RouteCardOperation, as: 'operations' }, 'operation_sequence_no', 'ASC']]
  });
  if (!record) return sendError(res, 'Route Card not found', 404);

  // Ensure operation_name is populated (fallback lookup from OperationName master)
  const json = typeof record.toJSON === 'function' ? record.toJSON() : record;
  const ops = Array.isArray(json.operations) ? json.operations : [];
  const missing = ops.filter((o) => !o.operation_name && o.operation_code).map((o) => String(o.operation_code));
  const uniqueCodes = Array.from(new Set(missing));
  if (uniqueCodes.length > 0) {
    const rows = await db.OperationName.findAll({
      where: {
        company_id: json.company_id,
        operation_code: { [Op.in]: uniqueCodes }
      },
      attributes: ['operation_code', 'description', 'department_id'],
      include: [{ model: db.Department, as: 'department', attributes: ['id', 'department_code', 'department_name'], required: false }]
    });
    const map = new Map(rows.map((r) => [String(r.operation_code), r.description || '']));
    const deptMap = new Map(
      rows.map((r) => [
        String(r.operation_code),
        r.department
          ? {
            id: r.department.id,
            department_code: r.department.department_code,
            department_name: r.department.department_name,
          }
          : null,
      ])
    );
    json.operations = ops.map((o) => ({
      ...o,
      operation_name: o.operation_name || map.get(String(o.operation_code)) || null,
      department: o.department || deptMap.get(String(o.operation_code)) || null,
    }));
  }

  json.final_qc_status = json?.finalInspection?.status || null;
  const {
    rawMaterialRows,
    nestingRawMaterialLineByMaterialId,
    nestingRawMaterialLineByPartMaterialId
  } = await buildNestingRawMaterialDisplayMaps(json?.production_order_id);

  const nestingStoreSheetMaterialIdsOrdered = [];
  const nestingSheetIdSet = new Set();
  for (const r of rawMaterialRows) {
    if (r.nesting_stores_request_line_id == null || r.material_id == null) continue;
    const id = parseInt(r.material_id, 10);
    if (!Number.isFinite(id) || id <= 0 || nestingSheetIdSet.has(id)) continue;
    nestingSheetIdSet.add(id);
    nestingStoreSheetMaterialIdsOrdered.push(id);
  }
  const hasNestingStoreRequestLines = nestingStoreSheetMaterialIdsOrdered.length > 0;

  const nestingSuggestedQtyByMaterialId = new Map();
  for (const r of rawMaterialRows) {
    if (r.nesting_stores_request_line_id == null || r.material_id == null) continue;
    const id = parseInt(r.material_id, 10);
    if (!Number.isFinite(id)) continue;
    const q = round3(r.nesting_confirmed_qty ?? 0);
    nestingSuggestedQtyByMaterialId.set(
      id,
      round3((nestingSuggestedQtyByMaterialId.get(id) || 0) + q)
    );
  }

  let processMap = {};
  const rawSwitches = json?.raw_material_process_switches;
  if (rawSwitches && typeof rawSwitches === 'object' && !Array.isArray(rawSwitches)) {
    processMap = rawSwitches;
  } else if (typeof rawSwitches === 'string' && rawSwitches.trim()) {
    try {
      const parsed = JSON.parse(rawSwitches);
      if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
        processMap = parsed;
      }
    } catch (_) {
      processMap = {};
    }
  }
  json.raw_material_nesting_rows = rawMaterialRows.map((r) => {
    const proc = processMap[r.key] || {};
    return {
      ...r,
      turret_punch_press: Boolean(proc.turret_punch_press),
      laser: Boolean(proc.laser),
      bending: Boolean(proc.bending)
    };
  });
  json.raw_material_process_switches = processMap;

  const poComponentQtyByMaterialId = new Map();
  (json?.productionOrder?.components || []).forEach((c) => {
    const key = String(c?.material_id || '');
    if (!key) return;
    const prev = poComponentQtyByMaterialId.get(key) || 0;
    poComponentQtyByMaterialId.set(key, prev + (parseFloat(c?.required_quantity || 0) || 0));
  });

  const routingOperations = json?.workOrder?.routing_id
    ? await db.RoutingOperation.findAll({
      where: { routing_id: json.workOrder.routing_id, status: 'ACTIVE' },
      attributes: ['id', 'operation_number', 'operation_code', 'operation_materials_json'],
      order: [['operation_number', 'ASC'], ['id', 'ASC']]
    })
    : [];
  const routingMaterialIds = Array.from(
    new Set(
      routingOperations.flatMap((op) => parseOperationMaterialIds(op.operation_materials_json))
    )
  );
  const routingMaterials = routingMaterialIds.length > 0
    ? await db.Material.findAll({
        where: { id: { [Op.in]: routingMaterialIds } },
        attributes: [
          'id',
          'material_number',
          'description',
          'length',
          'width',
          'height',
          'thickness',
          'drawing_number',
        ],
        include: [{ model: db.UOM, as: 'baseUOM', attributes: ['id', 'uom_code'], required: false }]
      })
    : [];
  const routingMaterialMap = new Map(routingMaterials.map((m) => [String(m.id), m]));

  const compMaterialIds = [
    ...new Set(
      (json?.productionOrder?.components || [])
        .map((c) => parseInt(c.material_id, 10))
        .filter((id) => Number.isFinite(id) && id > 0)
    )
  ];
  let poFlowMaterials = [];
  if (compMaterialIds.length > 0) {
    poFlowMaterials = await db.Material.findAll({
      where: {
        id: { [Op.in]: compMaterialIds },
        material_type: { [Op.in]: ['RAW_MATERIAL', 'CONSUMABLE', 'PACKAGING'] }
      },
      attributes: [
        'id',
        'material_number',
        'description',
        'material_type',
        'length',
        'width',
        'height',
        'thickness',
        'dimension_unit',
      ],
      include: [{ model: db.UOM, as: 'baseUOM', attributes: ['id', 'uom_code'], required: false }]
    });
  }

  const operationMaterialRows = [];
  const releasedQty = parseFloat(json?.production_qty || 0) || 0;
  const poOrderQty = parseFloat(json?.productionOrder?.order_quantity || 0) || 0;
  let seqNo = 1;
  let lastOperationKey = null;
  routingOperations.forEach((op) => {
    const opMaterials = parseOperationMaterialsWithQty(op.operation_materials_json);
    const currentOperationKey = `${op.operation_number || ''}|${op.operation_code || ''}`;
    opMaterials.forEach((opMat) => {
      const mat = routingMaterialMap.get(String(opMat.material_id));
      if (!mat) return;
      const routingPerSetQty = opMat.qty != null ? (parseFloat(opMat.qty) || 0) : null;
      const poTotalQty = poComponentQtyByMaterialId.has(String(mat.id))
        ? (parseFloat(poComponentQtyByMaterialId.get(String(mat.id))) || 0)
        : null;
      const fallbackPerSetQty =
        poTotalQty != null && poOrderQty > 0
          ? (poTotalQty / poOrderQty)
          : null;
      const perSetQty = routingPerSetQty != null ? routingPerSetQty : fallbackPerSetQty;
      const releasedBasedQty = perSetQty != null ? round3(perSetQty * releasedQty) : null;
      operationMaterialRows.push({
        seq: seqNo,
        material_id: mat.id,
        op_seq: lastOperationKey === currentOperationKey ? '' : (op.operation_number || null),
        op_code: lastOperationKey === currentOperationKey ? '' : (op.operation_code || ''),
        material: mat.description || mat.material_number || '',
        drawing_no: mat.drawing_number || '',
        qty: releasedBasedQty,
        uom: mat.baseUOM?.uom_code || '',
        length: mat.length != null ? round3(mat.length) : null,
        width: mat.width != null ? round3(mat.width) : null,
        height: mat.height != null ? round3(mat.height) : null,
        thickness: mat.thickness != null ? round3(mat.thickness) : null
      });
      seqNo += 1;
      lastOperationKey = currentOperationKey;
    });
  });
  json.operation_material_rows = operationMaterialRows;

  const poIdForWip = json.production_order_id;
  json.production_wip_balances = [];
  json.operation_wip_materials = {};
  if (poIdForWip) {
    const allowedFastenerIdSet = await loadAllowedFastenerMaterialIdSetForProductionOrder(poIdForWip);
    const balRows = await productionWipService.listBalancesForProductionOrder(poIdForWip);
    const balMap = new Map(balRows.map((b) => [String(b.material_id), b]));
    const balRowsForApi =
      hasNestingStoreRequestLines
        ? balRows.filter((b) => {
            const j = typeof b.toJSON === 'function' ? b.toJSON() : b;
            const mat = j.material || null;
            if (isFastenerForWipDisplay(mat)) {
              const mid = parseInt(j.material_id, 10);
              return Number.isFinite(mid) && allowedFastenerIdSet.has(mid);
            }
            const mb = parseInt(j.material_id, 10);
            return Number.isFinite(mb) && nestingSheetIdSet.has(mb);
          })
        : // No nesting→stores sheet lines: do not show raw material pool; allow only fasteners sent to stores via PO MR.
          balRows.filter((b) => {
            const j = typeof b.toJSON === 'function' ? b.toJSON() : b;
            const mat = j.material || null;
            if (!isFastenerForWipDisplay(mat)) return false;
            const mid = parseInt(j.material_id, 10);
            return Number.isFinite(mid) && allowedFastenerIdSet.has(mid);
          });
    json.production_wip_balances = balRowsForApi.map((b) => {
      const j = typeof b.toJSON === 'function' ? b.toJSON() : b;
      const mat = j.material || null;
      const midBal = parseInt(j.material_id, 10);
      const nestBal = resolveNestingDisplayForWipMaterial(
        midBal,
        mat,
        nestingRawMaterialLineByPartMaterialId,
        nestingRawMaterialLineByMaterialId
      );
      const displayBal = nestBal ? nestBal : buildWipMaterialDisplayString(mat);
      return {
        material_id: j.material_id,
        material_number: mat?.material_number || '',
        description: mat?.description || '',
        material_type: mat?.material_type || '',
        material_category: wipMaterialCategoryLabel(mat),
        material_display: displayBal,
        wip_row_kind: isFastenerForWipDisplay(mat) ? 'FASTENER' : 'RAW_MATERIAL',
        available_qty: round3(j.available_qty),
        uom_id: j.uom_id || j.uom?.id || null,
        uom_code: j.uom?.uom_code || ''
      };
    });

    const opIds = (json.operations || []).map((o) => o.id).filter((id) => id > 0);
    let allCons = [];
    if (opIds.length > 0) {
      allCons = await db.RouteCardOperationMaterialConsumption.findAll({
        where: { route_card_operation_id: { [Op.in]: opIds } },
        include: [
          { model: db.Material, as: 'material', attributes: ['id', 'material_number', 'description'] },
          { model: db.UOM, as: 'uom', attributes: ['id', 'uom_code'], required: false }
        ]
      });
    }
    const consByOp = new Map();
    for (const c of allCons) {
      const oid = c.route_card_operation_id;
      if (!consByOp.has(oid)) consByOp.set(oid, new Map());
      consByOp.get(oid).set(String(c.material_id), c);
    }

    const extraMaterialIds = new Set();
    for (const c of allCons) {
      if (!routingMaterialMap.has(String(c.material_id))) extraMaterialIds.add(c.material_id);
    }
    let extraMaterials = [];
    if (extraMaterialIds.size > 0) {
      extraMaterials = await db.Material.findAll({
        where: { id: { [Op.in]: Array.from(extraMaterialIds) } },
        attributes: [
          'id',
          'material_number',
          'description',
          'material_type',
          'length',
          'width',
          'height',
          'thickness',
          'dimension_unit',
        ],
        include: [{ model: db.UOM, as: 'baseUOM', attributes: ['id', 'uom_code'], required: false }]
      });
    }
    const fullMaterialMap = new Map(routingMaterialMap);
    extraMaterials.forEach((m) => fullMaterialMap.set(String(m.id), m));
    poFlowMaterials.forEach((m) => fullMaterialMap.set(String(m.id), m));

    const missingNestingMatIds = nestingStoreSheetMaterialIdsOrdered.filter(
      (id) => !fullMaterialMap.has(String(id))
    );
    if (missingNestingMatIds.length > 0) {
      const nestingOnlyMats = await db.Material.findAll({
        where: { id: { [Op.in]: missingNestingMatIds } },
        attributes: [
          'id',
          'material_number',
          'description',
          'material_type',
          'length',
          'width',
          'height',
          'thickness',
          'dimension_unit',
        ],
        include: [{ model: db.UOM, as: 'baseUOM', attributes: ['id', 'uom_code'], required: false }],
      });
      nestingOnlyMats.forEach((m) => fullMaterialMap.set(String(m.id), m));
    }

    const findRoutingOpForRcOp = (rcOp) => {
      const seq = parseInt(rcOp.operation_sequence_no, 10);
      const code = String(rcOp.operation_code || '').trim().toLowerCase();
      let ro = routingOperations.find(
        (r) =>
          parseInt(r.operation_number, 10) === seq &&
          String(r.operation_code || '').trim().toLowerCase() === code
      );
      if (!ro) ro = routingOperations.find((r) => parseInt(r.operation_number, 10) === seq);
      return ro || null;
    };

    for (const rcOp of json.operations || []) {
      const ro = findRoutingOpForRcOp(rcOp);
      const opMats = ro ? parseOperationMaterialsWithQty(ro.operation_materials_json) : [];
      const consMap = consByOp.get(rcOp.id) || new Map();
      const matIdsOrdered = [];
      const matSeen = new Set();
      const pushMaterialId = (rawId) => {
        const n = parseInt(rawId, 10);
        if (!Number.isFinite(n) || n <= 0 || matSeen.has(n)) return;
        matSeen.add(n);
        matIdsOrdered.push(n);
      };

      if (hasNestingStoreRequestLines) {
        nestingStoreSheetMaterialIdsOrdered.forEach((id) => pushMaterialId(id));
      } else {
        poFlowMaterials.forEach((m) => pushMaterialId(m.id));
        (opMats || []).forEach((m) => {
          if (m.material_id > 0) pushMaterialId(m.material_id);
        });
      }

      poFlowMaterials.forEach((m) => {
        if (isFastenerForWipDisplay(m)) pushMaterialId(m.id);
      });
      (opMats || []).forEach((om) => {
        const rm = routingMaterialMap.get(String(om.material_id));
        if (rm && isFastenerForWipDisplay(rm)) pushMaterialId(om.material_id);
      });
      for (const mid of consMap.keys()) pushMaterialId(mid);

      const rows = [];
      for (const materialId of matIdsOrdered) {
        const mat = fullMaterialMap.get(String(materialId));
        if (!mat) continue;
        const opMat = opMats.find((m) => Number(m.material_id) === Number(materialId));
        const routingPerSetQty = opMat && opMat.qty != null ? (parseFloat(opMat.qty) || 0) : null;
        const poTotalQty = poComponentQtyByMaterialId.has(String(mat.id))
          ? parseFloat(poComponentQtyByMaterialId.get(String(mat.id))) || 0
          : null;
        const fallbackPerSetQty =
          poTotalQty != null && poOrderQty > 0 ? poTotalQty / poOrderQty : null;
        const perSetQty = routingPerSetQty != null ? routingPerSetQty : fallbackPerSetQty;
        let releasedBasedQty = perSetQty != null ? round3(perSetQty * releasedQty) : null;
        if (hasNestingStoreRequestLines && nestingSheetIdSet.has(mat.id)) {
          const nq = nestingSuggestedQtyByMaterialId.get(mat.id);
          if (nq != null && nq > 0) {
            releasedBasedQty = nq;
          }
        }

        const bal = balMap.get(String(materialId));
        const available = bal ? round3(bal.available_qty) : 0;
        const cRow = consMap.get(String(materialId));
        const consumed = cRow ? round3(cRow.consumed_qty) : 0;
        const scrap = cRow ? round3(cRow.scrap_qty) : 0;

        const cj = cRow && typeof cRow.toJSON === 'function' ? cRow.toJSON() : cRow;
        const snapLine =
          cj?.material_display_snapshot != null && String(cj.material_display_snapshot).trim() !== ''
            ? String(cj.material_display_snapshot).trim()
            : '';
        const nestLine = resolveNestingDisplayForWipMaterial(
          mat.id,
          mat,
          nestingRawMaterialLineByPartMaterialId,
          nestingRawMaterialLineByMaterialId
        );
        let displayLine = snapLine;
        if (!displayLine) {
          displayLine = nestLine || buildWipMaterialDisplayString(mat);
        }
        const dbWipClass = String(cj?.wip_material_class || '').toUpperCase();
        const wipKind =
          dbWipClass === 'FASTENER' || dbWipClass === 'RAW_MATERIAL'
            ? dbWipClass
            : isFastenerForWipDisplay(mat)
              ? 'FASTENER'
              : 'RAW_MATERIAL';

        rows.push({
          material_id: mat.id,
          material_number: mat.material_number || '',
          description: mat.description || '',
          material_type: mat.material_type || '',
          material_category: wipKind === 'FASTENER' ? 'Fastener' : 'Raw material',
          material_display: displayLine,
          material_display_snapshot: snapLine || null,
          wip_row_kind: wipKind,
          uom_id: cRow?.uom_id || mat.baseUOM?.id || null,
          uom_code: mat.baseUOM?.uom_code || cRow?.uom?.uom_code || '',
          suggested_qty: releasedBasedQty,
          available_qty: available,
          consumed_qty: consumed,
          scrap_qty: scrap,
          balance_qty: round3(available - consumed - scrap)
        });
      }
      json.operation_wip_materials[String(rcOp.id)] = rows;
    }
  }

  sendResponse(res, 200, true, 'Route Card retrieved successfully', json);
});

exports.getOperationComponentTracking = asyncHandler(async (req, res) => {
  const { id } = req.params; // route_card_operation_id
  const opRow = await db.RouteCardOperation.findByPk(id, {
    attributes: ['id', 'route_card_id', 'operation_sequence_no', 'operation_code'],
    include: [
      {
        model: db.RouteCardOperationComponentTracking,
        as: 'componentTrackings',
        required: false,
        include: [
          { model: db.Material, as: 'material', attributes: ['id', 'material_number', 'description'], required: false },
        ],
      },
    ],
  });
  if (!opRow) return sendError(res, 'Route card operation not found', 404);
  const json = typeof opRow.toJSON === 'function' ? opRow.toJSON() : opRow;
  sendResponse(res, 200, true, 'Operation component tracking loaded', {
    operation: {
      id: json.id,
      route_card_id: json.route_card_id,
      operation_sequence_no: json.operation_sequence_no,
      operation_code: json.operation_code,
    },
    items: json.componentTrackings || [],
  });
});

exports.saveOperationComponentTracking = asyncHandler(async (req, res) => {
  const { id } = req.params; // route_card_operation_id
  const userId = req.user?.userId || null;
  const items = Array.isArray(req.body?.items) ? req.body.items : [];

  const opRow = await db.RouteCardOperation.findByPk(id, {
    attributes: ['id', 'route_card_id', 'status'],
  });
  if (!opRow) return sendError(res, 'Route card operation not found', 404);
  const opStatus = String(opRow.status || '').toUpperCase();
  if (opStatus === 'PENDING') {
    return sendError(res, 'Start the operation first, then save completed quantity in Components.', 400);
  }

  const tx = await db.sequelize.transaction();
  try {
    const saved = [];
    for (const row of items) {
      const materialId = parseInt(row?.material_id, 10);
      if (!Number.isFinite(materialId) || materialId <= 0) continue;

      const requiredQty = parseFloat(row?.required_quantity ?? 0) || 0;
      const completedQty = parseFloat(row?.completed_quantity ?? 0) || 0;
      if (completedQty - requiredQty > 0.0005) {
        await tx.rollback();
        return sendError(
          res,
          `Completed quantity cannot exceed required quantity for material ${materialId}.`,
          400
        );
      }

      const startedAt = row?.started_at ? new Date(row.started_at) : null;
      const endedAt = row?.ended_at ? new Date(row.ended_at) : null;
      if (completedQty > 0) {
        if (!startedAt || !endedAt) {
          await tx.rollback();
          return sendError(
            res,
            `Start time and End time are required for material ${materialId} when Completed quantity is entered.`,
            400
          );
        }
      }
      if (startedAt && Number.isNaN(startedAt.getTime())) {
        await tx.rollback();
        return sendError(res, `Invalid start time for material ${materialId}.`, 400);
      }
      if (endedAt && Number.isNaN(endedAt.getTime())) {
        await tx.rollback();
        return sendError(res, `Invalid end time for material ${materialId}.`, 400);
      }
      if (startedAt && endedAt && endedAt.getTime() < startedAt.getTime()) {
        await tx.rollback();
        return sendError(res, `End time cannot be before start time for material ${materialId}.`, 400);
      }

      const totalMinutes = minutesBetween(startedAt, endedAt);

      const [rec] = await db.RouteCardOperationComponentTracking.findOrCreate({
        where: { route_card_operation_id: opRow.id, material_id: materialId },
        defaults: {
          route_card_id: opRow.route_card_id,
          route_card_operation_id: opRow.id,
          material_id: materialId,
          required_quantity: requiredQty,
          completed_quantity: completedQty,
          started_at: startedAt,
          ended_at: endedAt,
          total_minutes: totalMinutes,
          created_by: userId,
          updated_by: userId,
        },
        transaction: tx,
      });

      await rec.update(
        {
          required_quantity: requiredQty,
          completed_quantity: completedQty,
          started_at: startedAt,
          ended_at: endedAt,
          total_minutes: totalMinutes,
          updated_by: userId,
        },
        { transaction: tx }
      );
      saved.push(rec);
    }

    await tx.commit();
    const out = await db.RouteCardOperationComponentTracking.findAll({
      where: { route_card_operation_id: opRow.id },
      include: [{ model: db.Material, as: 'material', attributes: ['id', 'material_number', 'description'], required: false }],
      order: [['material_id', 'ASC']],
    });
    sendResponse(res, 200, true, 'Operation component tracking saved', { items: out });
  } catch (e) {
    await tx.rollback();
    sendError(res, e.message || 'Failed to save operation component tracking', 400);
  }
});

exports.updateRawMaterialProcesses = asyncHandler(async (req, res) => {
  const { id } = req.params;
  const record = await db.RouteCard.findByPk(id);
  if (!record) return sendError(res, 'Route Card not found', 404);

  const userCompanyId = req.user?.companyId ?? req.user?.company_id;
  if (
    userCompanyId != null &&
    !req.user?.isSuperAdmin &&
    !req.user?.is_super_admin &&
    Number(record.company_id) !== Number(userCompanyId)
  ) {
    return sendError(res, 'Forbidden', 403);
  }

  const incoming = req.body?.raw_material_process_switches;
  if (!incoming || typeof incoming !== 'object' || Array.isArray(incoming)) {
    return sendError(res, 'raw_material_process_switches object is required', 400);
  }

  const normalized = {};
  Object.entries(incoming).forEach(([key, value]) => {
    if (!key || typeof value !== 'object' || Array.isArray(value)) return;
    const mid = parseInt(value.material_id, 10);
    const entry = {
      turret_punch_press: Boolean(value.turret_punch_press),
      laser: Boolean(value.laser),
      bending: Boolean(value.bending)
    };
    if (Number.isFinite(mid) && mid > 0) entry.material_id = mid;
    normalized[key] = entry;
  });

  await record.update({
    raw_material_process_switches: normalized,
    updated_by: req.user?.id ?? req.user?.userId ?? null
  });

  sendResponse(res, 200, true, 'Raw material process switches saved', {
    raw_material_process_switches: normalized
  });
});

exports.updatePdfSignOffs = asyncHandler(async (req, res) => {
  const { id } = req.params;
  const record = await db.RouteCard.findByPk(id);
  if (!record) return sendError(res, 'Route Card not found', 404);

  const userCompanyId = req.user?.companyId ?? req.user?.company_id;
  if (
    userCompanyId != null &&
    !req.user?.isSuperAdmin &&
    !req.user?.is_super_admin &&
    Number(record.company_id) !== Number(userCompanyId)
  ) {
    return sendError(res, 'Forbidden', 403);
  }

  const parsed = parsePdfSignOffsJson(req.body);
  const allIds = [
    ...parsed.verified_by_user_ids,
    ...parsed.accountant_user_ids,
    ...parsed.qc_incharge_user_ids,
  ];
  const uniqueIds = [...new Set(allIds)];
  if (uniqueIds.length > 0) {
    const validCount = await db.User.count({
      where: {
        id: { [Op.in]: uniqueIds },
        company_id: record.company_id,
        status: 'ACTIVE',
        is_active: true,
      },
    });
    if (validCount !== uniqueIds.length) {
      return sendError(res, 'One or more selected users are invalid for this company', 400);
    }
  }

  await record.update({
    pdf_sign_offs: parsed,
    updated_by: req.user?.id ?? null,
  });

  sendResponse(res, 200, true, 'PDF sign-offs saved', { pdf_sign_offs: parsed });
});

exports.generatePDF = asyncHandler(async (req, res) => {
  const { id } = req.params;

  const record = await db.RouteCard.findByPk(id, {
    include: [
      {
        model: db.WorkOrder,
        as: 'workOrder',
        attributes: ['id', 'work_order_number', 'work_order_quantity', 'routing_id', 'created_by', 'approved_by'],
        include: [
          { model: db.User, as: 'createdByUser', attributes: ['id', 'username', 'first_name', 'last_name'], required: false },
          { model: db.User, as: 'approvedByUser', attributes: ['id', 'username', 'first_name', 'last_name'], required: false }
        ]
      },
      {
        model: db.ProductionOrder,
        as: 'productionOrder',
        attributes: ['id', 'production_order_number', 'customer_po_id', 'order_quantity'],
        include: [
          {
            model: db.CustomerPO,
            as: 'customerPO',
            attributes: ['id'],
            include: [
              {
                model: db.BusinessPartner,
                as: 'customer',
                attributes: ['id', 'name', 'legal_name', 'bp_number'],
              },
            ],
          },
          {
            model: db.ProductionOrderComponent,
            as: 'components',
            attributes: ['id', 'required_quantity'],
            required: false,
            include: [
              { model: db.BOMComponent, as: 'bomComponent', attributes: ['id', 'item_number'], required: false },
              { model: db.Material, as: 'material', attributes: ['id', 'material_number', 'description'], required: false },
              { model: db.UOM, as: 'uom', attributes: ['id', 'uom_code'], required: false }
            ]
          }
        ]
      },
      { model: db.Material, as: 'finishedMaterial', attributes: ['id', 'material_number', 'description', 'drawing_number', 'revision_number'] },
      {
        model: db.BOMHeader,
        as: 'bom',
        attributes: ['id', 'bom_number', 'drawing_number', 'revision_number'],
        include: [
          {
            model: db.BOMComponent,
            as: 'components',
            where: { status: 'ACTIVE' },
            required: false,
            include: [
              { model: db.Material, as: 'material', attributes: ['id', 'material_number', 'description'] },
              { model: db.UOM, as: 'componentUOM', attributes: ['id', 'uom_code'] }
            ]
          }
        ]
      },
      {
        model: db.RouteCardOperation,
        as: 'operations',
        include: [
          { model: db.ProductionOperator, as: 'operator', attributes: ['id', 'operator_name'] },
          { model: db.WorkCenter, as: 'workCenter', attributes: ['id', 'work_center_code', 'description'] },
          {
            model: db.ProductionQC,
            as: 'productionQC',
            required: false,
            attributes: ['id', 'qc_by', 'qc_by_ids', 'qc_at', 'status'],
            include: [{ model: db.User, as: 'qcByUser', attributes: ['id', 'username', 'first_name', 'last_name'], required: false }]
          }
        ]
      },
      { model: db.User, as: 'createdByUser', attributes: ['id', 'username', 'first_name', 'last_name'], required: false },
      { model: db.User, as: 'updatedByUser', attributes: ['id', 'username', 'first_name', 'last_name'], required: false }
    ],
    order: [
      [{ model: db.RouteCardOperation, as: 'operations' }, 'operation_sequence_no', 'ASC'],
      [{ model: db.BOMHeader, as: 'bom' }, { model: db.BOMComponent, as: 'components' }, 'item_number', 'ASC'],
      [{ model: db.ProductionOrder, as: 'productionOrder' }, { model: db.ProductionOrderComponent, as: 'components' }, { model: db.BOMComponent, as: 'bomComponent' }, 'item_number', 'ASC']
    ]
  });

  if (!record) return sendError(res, 'Route Card not found', 404);

  const formatDate = (v) => {
    if (!v) return '';
    const d = new Date(v);
    if (Number.isNaN(d.getTime())) return '';
    const day = String(d.getDate()).padStart(2, '0');
    const month = String(d.getMonth() + 1).padStart(2, '0');
    const year = d.getFullYear();
    return `${day}-${month}-${year}`;
  };
  const formatDateTime = (v) => {
    if (!v) return '';
    const d = new Date(v);
    if (Number.isNaN(d.getTime())) return '';
    const day = String(d.getDate()).padStart(2, '0');
    const month = String(d.getMonth() + 1).padStart(2, '0');
    const year = d.getFullYear();
    const hh = String(d.getHours()).padStart(2, '0');
    const mm = String(d.getMinutes()).padStart(2, '0');
    return `${day}-${month}-${year} ${hh}:${mm}`;
  };
  const fmtNum = (v, d = 3) => {
    const n = parseFloat(v || 0);
    return Number.isFinite(n) ? n.toFixed(d) : '0.000';
  };

  const safeRc = String(record.route_card_number || record.id).replace(/[<>:"/\\|?*\x00-\x1F]/g, '');
  const filename = `RouteCard_${safeRc}.pdf`;

  // Fetch all async data BEFORE starting PDF stream to avoid incomplete chunk responses on mid-stream errors.
  const printHeader = await db.PrintHeader.findOne({
    where: { company_id: record.company_id, status: 'ACTIVE' },
    order: [['id', 'DESC']]
  });
  const latestSalesInvoice = record.productionOrder?.customer_po_id
    ? await db.SalesInvoice.findOne({
        where: {
          company_id: record.company_id,
          customer_po_id: record.productionOrder.customer_po_id,
          status: { [Op.ne]: 'CANCELLED' }
        },
        attributes: ['id', 'invoice_number', 'invoice_date'],
        order: [['invoice_date', 'DESC'], ['id', 'DESC']]
      })
    : null;
  const routingOperations = record.workOrder?.routing_id
    ? await db.RoutingOperation.findAll({
        where: { routing_id: record.workOrder.routing_id, status: 'ACTIVE' },
        attributes: ['id', 'operation_number', 'operation_code', 'operation_description', 'operation_materials_json'],
        order: [['operation_number', 'ASC'], ['id', 'ASC']]
      })
    : [];

  const parseOperationMaterialIds = (raw) =>
    parseOperationMaterialsWithQty(raw).map((r) => r.material_id);
  const routingMaterialIds = Array.from(
    new Set(
      routingOperations.flatMap((op) => parseOperationMaterialIds(op.operation_materials_json))
    )
  );
  const routingMaterials = routingMaterialIds.length > 0
    ? await db.Material.findAll({
        where: { id: { [Op.in]: routingMaterialIds } },
        attributes: [
          'id',
          'material_number',
          'description',
          'length',
          'width',
          'height',
          'thickness',
          'dimension_unit',
          'material_type',
        ],
        include: [{ model: db.UOM, as: 'baseUOM', attributes: ['id', 'uom_code'], required: false }]
      })
    : [];
  const routingMaterialMap = new Map(routingMaterials.map((m) => [String(m.id), m]));
  const poComponentQtyByMaterialId = new Map();
  (record?.productionOrder?.components || []).forEach((c) => {
    const key = String(c?.material_id || '');
    if (!key) return;
    const prev = poComponentQtyByMaterialId.get(key) || 0;
    poComponentQtyByMaterialId.set(key, prev + (parseFloat(c?.required_quantity || 0) || 0));
  });
  const nestingRowsForPdf = await buildNestingSheetRowsByProductionOrder(record?.production_order_id);
  let processMap = {};
  const rawSwitches = record?.raw_material_process_switches;
  if (rawSwitches && typeof rawSwitches === 'object' && !Array.isArray(rawSwitches)) {
    processMap = rawSwitches;
  } else if (typeof rawSwitches === 'string' && rawSwitches.trim()) {
    try {
      const parsed = JSON.parse(rawSwitches);
      if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) processMap = parsed;
    } catch (_) {
      processMap = {};
    }
  }
  const rawMaterialPdfRows = nestingRowsForPdf.map((r, idx) => {
    const proc = processMap[r.key] || {};
    const processNames = [
      proc.turret_punch_press ? 'Turret Punch Press' : '',
      proc.laser ? 'Laser' : '',
      proc.bending ? 'Bending' : ''
    ].filter(Boolean).join(', ');
    const qu = r.quantity_uom_code && String(r.quantity_uom_code).trim();
    return {
      seq: String(idx + 1),
      raw_material: r.raw_material || '',
      uom: qu || '—',
      nesting_confirmed_qty: fmtNum(r.nesting_confirmed_qty),
      processes: processNames || '-'
    };
  });

  const doc = new PDFDocument({ margin: 24, size: 'A4' });
  res.setHeader('Content-Type', 'application/pdf');
  res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
  doc.pipe(res);

  const pageW = doc.page.width;
  const pageH = doc.page.height;
  const margin = 24;
  const contentW = pageW - margin * 2;
  const contentH = pageH - margin * 2;

  const customerName =
    record.productionOrder?.customerPO?.customer?.name ||
    record.productionOrder?.customerPO?.customer?.legal_name ||
    '';
  const customerCode = record.productionOrder?.customerPO?.customer?.bp_number || '';
  const orderQtyHeader =
    record.productionOrder?.order_quantity != null && record.productionOrder?.order_quantity !== ''
      ? fmtNum(record.productionOrder.order_quantity)
      : fmtNum(record.workOrder?.work_order_quantity);
  const releasedQtyHeader = fmtNum(record.production_qty);
  const oneLine = (value) => String(value != null ? value : '').replace(/\s+/g, ' ').trim();
  const drawingNo = oneLine(record.bom?.drawing_number || record.finishedMaterial?.drawing_number || '');
  let itemDesc = oneLine(record.finishedMaterial?.description || '');
  if (drawingNo && itemDesc) {
    const escaped = drawingNo.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
    itemDesc = itemDesc.replace(new RegExp(`^${escaped}\\s*[-–—]?\\s*`, 'i'), '').trim();
  }
  const revisionNo = oneLine(record.bom?.revision_number || record.finishedMaterial?.revision_number || '');
  // Route card generated date = route card creation timestamp (fallback to start_date if needed)
  const generatedDate = formatDate(record.created_at || record.createdAt || record.start_date);
  const invoiceNo = latestSalesInvoice?.invoice_number || '';
  const signOffs = parsePdfSignOffsJson(record.pdf_sign_offs);
  const cid = record.company_id;
  const [verifiedByPdf, accountantPdf, qcInchargePdf] = await Promise.all([
    formatUserNamesFromIds(signOffs.verified_by_user_ids, cid),
    formatUserNamesFromIds(signOffs.accountant_user_ids, cid),
    formatUserNamesFromIds(signOffs.qc_incharge_user_ids, cid),
  ]);

  const drawPageBox = () => {
    doc.rect(margin, margin, contentW, contentH).stroke('#000');
  };

  const resolveLogoPath = (logoField) => {
    if (!logoField) return null;
    let fullPath = path.join(__dirname, '..', 'uploads', logoField);
    if (fs.existsSync(fullPath)) return fullPath;
    fullPath = path.join(__dirname, '..', logoField);
    if (fs.existsSync(fullPath)) return fullPath;
    return null;
  };
  const companyLogoPath = resolveLogoPath(printHeader?.logo_path);
  const certLogoPath = resolveLogoPath(printHeader?.quality_certification_logo_path);

  const drawCompanyHeader = (y) => {
    const h = 105;
    doc.rect(margin, y, contentW, h).stroke('#000');
    const sidePadding = 4;
    const logoBoxW = Math.min(200, contentW * 0.34);
    const certBoxW = Math.min(160, contentW * 0.26);
    const boxH = h - 4;

    const leftBlockW = logoBoxW + (sidePadding * 2);
    const rightBlockW = certBoxW + (sidePadding * 2);
    const centerX = margin + leftBlockW;
    const centerW = contentW - leftBlockW - rightBlockW;

    if (companyLogoPath) {
      try {
        // Match purchase order style: logo is prominent and close to the border.
        doc.image(companyLogoPath, margin + sidePadding, y + 2, {
          cover: [logoBoxW, boxH],
          align: 'center',
          valign: 'center'
        });
      } catch (e) { /* ignore image errors */ }
    }

    const title = (printHeader?.company_name || 'RAM INDUSTRIES').toUpperCase();
    const addressLines = String(printHeader?.address || '')
      .split(/\r?\n/)
      .map((v) => v.trim())
      .filter(Boolean);
    const lines = [
      ...addressLines,
      printHeader?.email ? `Email : ${printHeader.email}` : '',
      printHeader?.phone ? `Contact : ${printHeader.phone}` : '',
      printHeader?.gstin ? `GSTIN : ${printHeader.gstin}` : ''
    ].filter(Boolean);

    // Center-align and center-position details within right block.
    doc.font('Helvetica-Bold').fontSize(17);
    const titleH = doc.heightOfString(title, { width: centerW, align: 'center' });
    doc.font('Helvetica').fontSize(9);
    const linesH = lines.slice(0, 5).reduce((sum, line) => {
      const hLine = doc.heightOfString(line, { width: centerW, align: 'center' });
      return sum + Math.max(10, hLine + 1);
    }, 0);
    const totalH = titleH + 4 + linesH;
    const topY = y + Math.max(4, (h - totalH) / 2);

    doc.font('Helvetica-Bold').fontSize(17).text(title, centerX, topY, {
      width: centerW,
      align: 'center'
    });
    doc.font('Helvetica').fontSize(9);
    let ly = topY + titleH + 4;
    lines.slice(0, 5).forEach((line) => {
      const lineH = doc.heightOfString(line, { width: centerW, align: 'center' });
      doc.text(line, centerX, ly, { width: centerW, align: 'center' });
      ly += Math.max(10, lineH + 1);
    });

    if (certLogoPath) {
      try {
        const certX = margin + contentW - certBoxW - sidePadding;
        doc.image(certLogoPath, certX, y + 2, { fit: [certBoxW, boxH], align: 'center', valign: 'center' });
      } catch (e) { /* ignore image errors */ }
    }

    // Keep top and bottom outline visible over images.
    doc.save();
    doc.lineWidth(1).strokeColor('#000');
    doc.moveTo(margin, y).lineTo(margin + contentW, y).stroke();
    doc.moveTo(margin, y + h).lineTo(margin + contentW, y + h).stroke();
    doc.restore();
    return y + h;
  };

  const drawSectionTitle = (y, title) => {
    const h = 22;
    doc.rect(margin, y, contentW, h).stroke('#000');
    doc.font('Helvetica-Bold').fontSize(12).text(title, margin, y + 5, { width: contentW, align: 'center' });
    return y + h;
  };

  const drawMainHeader = (y) => {
    const leftRows = [
      ['Route Card No', record.route_card_number || ''],
      ['Date', generatedDate],
      ['Production Order No', record.productionOrder?.production_order_number || ''],
      ['Customer Code', customerCode],
      ['Customer Name', oneLine(customerName)],
      ['Verified By', verifiedByPdf || '—'],
      ['Accountant', accountantPdf || '—'],
      ['QC Incharge', qcInchargePdf || '—'],
    ];
    const rightRows = [
      ['Description', oneLine(itemDesc)],
      ['Drawing No', oneLine(drawingNo)],
      ['Revision No', oneLine(revisionNo)],
      ['Sales Invoice No', oneLine(invoiceNo || 'Not Created')],
      ['Order Qty', oneLine(orderQtyHeader)],
      ['Released Qty', oneLine(releasedQtyHeader)],
      null,
      null,
    ];
    const rowCount = Math.max(leftRows.length, rightRows.length);
    const leftX = margin + 8;
    const rightX = margin + contentW / 2 + 8;
    const colW = contentW / 2 - 16;
    const labelW = Math.min(112, Math.max(88, colW * 0.34));
    const colonW = 10;
    const valW = colW - labelW - colonW;

    const rowBlockHeight = (k, v) => {
      doc.font('Helvetica-Bold').fontSize(8.5);
      const kH = doc.heightOfString(String(k || ''), { width: labelW });
      doc.font('Helvetica').fontSize(8.5);
      const vH = doc.heightOfString(String(v != null && v !== '' ? v : ''), { width: valW, lineGap: 1 });
      return Math.max(kH, vH, 11);
    };

    const rowHeights = [];
    for (let i = 0; i < rowCount; i += 1) {
      const L = leftRows[i];
      const R = rightRows[i];
      let h = 0;
      if (L && R) h = Math.max(rowBlockHeight(L[0], L[1]), rowBlockHeight(R[0], R[1]));
      else if (L) h = rowBlockHeight(L[0], L[1]);
      else if (R) h = rowBlockHeight(R[0], R[1]);
      rowHeights.push(h + 6);
    }
    const totalH = rowHeights.reduce((sum, rh) => sum + rh, 0) + 16;
    doc.rect(margin, y, contentW, totalH).stroke('#000');

    let yy = y + 8;
    for (let i = 0; i < rowCount; i += 1) {
      const L = leftRows[i];
      const R = rightRows[i];
      const rh = rowHeights[i];
      if (L) {
        doc.fillColor('#000');
        doc.font('Helvetica-Bold').fontSize(8.5).text(String(L[0] || ''), leftX, yy, { width: labelW, align: 'left' });
        doc.text(':', leftX + labelW, yy, { width: colonW, align: 'center' });
        doc
          .font('Helvetica')
          .fontSize(8.5)
          .text(String(L[1] != null && L[1] !== '' ? L[1] : ''), leftX + labelW + colonW, yy, { width: valW, align: 'left', lineGap: 1 });
      }
      if (R) {
        doc.font('Helvetica-Bold').fontSize(8.5).text(String(R[0] || ''), rightX, yy, { width: labelW, align: 'left' });
        doc.text(':', rightX + labelW, yy, { width: colonW, align: 'center' });
        doc
          .font('Helvetica')
          .fontSize(8.5)
          .text(String(R[1] != null && R[1] !== '' ? R[1] : ''), rightX + labelW + colonW, yy, { width: valW, align: 'left', lineGap: 1 });
      }
      yy += rh;
    }
    return y + totalH;
  };

  // Pin the three signature boxes to the bottom of the page (above the outer border),
  // with a gap after the operation table when there is room.
  const drawSignOffFooter = (yAfterOperationTable) => {
    const boxH = 58;
    const minGapAfterTable = 20;
    const innerBottom = pageH - margin;
    const bottomPinnedTop = innerBottom - boxH;
    let boxTop = Math.max(yAfterOperationTable + minGapAfterTable, bottomPinnedTop);

    if (boxTop + boxH > innerBottom) {
      doc.addPage({ size: 'A4', margin: 24 });
      drawPageBox();
      boxTop = bottomPinnedTop;
    }

    const colW = contentW / 3;
    const titles = ['Managing Director', 'Technical Managing Director', 'Approved By'];
    doc.rect(margin, boxTop, contentW, boxH).stroke('#000');
    doc.moveTo(margin + colW, boxTop).lineTo(margin + colW, boxTop + boxH).stroke('#000');
    doc.moveTo(margin + colW * 2, boxTop).lineTo(margin + colW * 2, boxTop + boxH).stroke('#000');
    titles.forEach((title, i) => {
      const cx = margin + i * colW;
      const innerPad = 10;
      const textW = colW - innerPad * 2;
      doc.font('Helvetica-Bold').fontSize(8.5).fillColor('#000').text(title, cx + innerPad, boxTop + 10, {
        width: textW,
        align: 'center',
      });
    });
    return boxTop + boxH + 10;
  };

  const tableHeaderFontSize = 7.5;

  const measureTableHeaderHeight = (cols) => {
    doc.font('Helvetica-Bold').fontSize(tableHeaderFontSize);
    let maxH = 0;
    cols.forEach((c) => {
      const w = Math.max(4, (c.w || 0) - 6);
      const h = doc.heightOfString(String(c.l), { width: w, lineGap: 0.5 });
      maxH = Math.max(maxH, h);
    });
    return Math.max(26, Math.ceil(maxH) + 10);
  };

  const measureTableRowHeight = (cols, row, fontSize) => {
    doc.font('Helvetica').fontSize(fontSize);
    let maxH = 0;
    cols.forEach((c) => {
      const w = Math.max(4, (c.w || 0) - 4);
      const text = String(row[c.k] || '');
      const h = doc.heightOfString(text, { width: w, lineGap: 0.5 });
      maxH = Math.max(maxH, h);
    });
    return Math.max(16, Math.ceil(maxH) + 8);
  };

  const drawTableHeader = (yPos, cols) => {
    const headerH = measureTableHeaderHeight(cols);
    doc.rect(margin, yPos, contentW, headerH).fillAndStroke('#d8dde7', '#000');
    let x = margin;
    cols.forEach((c) => {
      doc.font('Helvetica-Bold').fontSize(tableHeaderFontSize).fillColor('#000').text(String(c.l), x + 3, yPos + 4, {
        width: Math.max(4, c.w - 6),
        lineGap: 0.5,
      });
      x += c.w;
      if (x < margin + contentW) doc.moveTo(x, yPos).lineTo(x, yPos + headerH).stroke('#000');
    });
    return yPos + headerH;
  };

  const drawTableRow = (yPos, cols, row, fontSize = 7.5) => {
    const rowH = measureTableRowHeight(cols, row, fontSize);
    doc.rect(margin, yPos, contentW, rowH).stroke('#000');
    let x = margin;
    cols.forEach((c) => {
      doc.font('Helvetica').fontSize(fontSize).fillColor('#000').text(String(row[c.k] || ''), x + 2, yPos + 4, {
        width: Math.max(4, c.w - 4),
        lineGap: 0.5,
      });
      x += c.w;
      if (x < margin + contentW) doc.moveTo(x, yPos).lineTo(x, yPos + rowH).stroke('#000');
    });
    return yPos + rowH;
  };

  const newPageForContinuation = (title, cols) => {
    doc.addPage({ size: 'A4', margin: 24 });
    drawPageBox();
    let ny = margin;
    ny = drawCompanyHeader(ny);
    ny = drawSectionTitle(ny, `ROUTE CARD - ${title}`);
    ny += 8;
    ny = drawTableHeader(ny, cols);
    return ny;
  };

  drawPageBox();
  let y = margin;
  y = drawCompanyHeader(y);
  y = drawSectionTitle(y, 'ROUTE CARD');
  y = drawMainHeader(y);
  y += 8;

  doc.font('Helvetica-Bold').fontSize(11).text('OPERATION ITEM', margin, y, { width: contentW, align: 'left' });
  y += 14;
  const rawBomCols = [
    { k: 'seq', l: 'S.No', w: 46 },
    { k: 'material', l: 'Item', w: 354 },
    { k: 'qty', l: 'Qty', w: 74 },
    { k: 'uom', l: 'UOM', w: 64 },
    { k: 'length', l: 'Length', w: 58 },
    { k: 'width', l: 'Width', w: 58 },
    { k: 'height', l: 'Height', w: 58 },
    { k: 'thickness', l: 'Thick', w: 66 }
  ];
  // Scale operation-material columns to fit printable width and avoid right-edge overlap.
  const rawBomSumW = rawBomCols.reduce((sum, c) => sum + (c.w || 0), 0) || 1;
  const bomScale = contentW / rawBomSumW;
  const bomCols = rawBomCols.map((c) => ({ ...c, w: Math.max(8, Math.round((c.w || 0) * bomScale)) }));
  const bomScaledSum = bomCols.reduce((sum, c) => sum + (c.w || 0), 0);
  bomCols[bomCols.length - 1].w += Math.max(-80, Math.min(80, Math.round(contentW - bomScaledSum)));
  y = drawTableHeader(y, bomCols);

  const bomGroups = [];
  const releasedQtyForPdf = parseFloat(record?.production_qty || 0) || 0;
  const poOrderQtyForPdf = parseFloat(record?.productionOrder?.order_quantity || 0) || 0;
  let seqNo = 1;
  routingOperations.forEach((op) => {
    const opMaterials = parseOperationMaterialsWithQty(op.operation_materials_json);
    const groupRows = [];
    opMaterials.forEach((opMat) => {
      const mat = routingMaterialMap.get(String(opMat.material_id));
      if (!mat) return;
      const routingPerSetQty = opMat.qty != null ? (parseFloat(opMat.qty) || 0) : null;
      const poTotalQty = poComponentQtyByMaterialId.has(String(mat.id))
        ? (parseFloat(poComponentQtyByMaterialId.get(String(mat.id))) || 0)
        : null;
      const fallbackPerSetQty =
        poTotalQty != null && poOrderQtyForPdf > 0
          ? (poTotalQty / poOrderQtyForPdf)
          : null;
      const perSetQty = routingPerSetQty != null ? routingPerSetQty : fallbackPerSetQty;
      const releasedBasedQty = perSetQty != null ? (perSetQty * releasedQtyForPdf) : null;
      groupRows.push({
        seq: String(seqNo),
        material: mat.description || mat.material_number || '',
        qty: releasedBasedQty != null ? fmtNum(releasedBasedQty) : '-',
        uom: mat.baseUOM?.uom_code || '',
        length: mat.length != null ? fmtNum(mat.length) : '-',
        width: mat.width != null ? fmtNum(mat.width) : '-',
        height: mat.height != null ? fmtNum(mat.height) : '-',
        thickness: mat.thickness != null ? fmtNum(mat.thickness) : '-'
      });
      seqNo += 1;
    });
    if (groupRows.length > 0) {
      bomGroups.push({
        opSeq: String(op.operation_number || '-'),
        opCode: op.operation_code || '-',
        rows: groupRows
      });
    }
  });
  if (bomGroups.length === 0) {
    bomGroups.push({
      opSeq: '-',
      opCode: '-',
      rows: [{
      seq: '-',
      material: 'No operation materials found in routing',
      qty: '-',
      uom: '-',
      length: '-',
      width: '-',
      height: '-',
      thickness: '-'
      }]
    });
  }

  bomGroups.forEach((group) => {
    const groupLabel = `Op Seq: ${group.opSeq}    Operation: ${group.opCode}`;
    doc.font('Helvetica-Bold').fontSize(8.5);
    const groupLabelH = Math.max(18, Math.ceil(doc.heightOfString(groupLabel, { width: contentW - 8 })) + 8);
    if (y + groupLabelH > pageH - margin - 30) y = newPageForContinuation('OPERATION ITEM', bomCols);
    doc.rect(margin, y, contentW, groupLabelH).fillAndStroke('#eef1f7', '#000');
    doc.fillColor('#000').text(groupLabel, margin + 4, y + 4, { width: contentW - 8 });
    y += groupLabelH;

    group.rows.forEach((r) => {
      const rowH = measureTableRowHeight(bomCols, r, 7.5);
      if (y + rowH > pageH - margin - 30) y = newPageForContinuation('OPERATION ITEM', bomCols);
      y = drawTableRow(y, bomCols, r, 7.5);
    });
  });

  y += 12;
  if (y + 80 > pageH - margin - 30) {
    doc.addPage({ size: 'A4', margin: 24 });
    drawPageBox();
    y = margin;
    y = drawCompanyHeader(y);
    y = drawSectionTitle(y, 'ROUTE CARD - RAW MATERIAL');
    y += 8;
  }
  doc.font('Helvetica-Bold').fontSize(11).text('RAW MATERIAL', margin, y, { width: contentW, align: 'left' });
  y += 14;
  // Portrait A4 ~547pt wide — fixed widths from landscape layout made the last columns negative; scale to contentW.
  const rawColsTemplate = [
    { k: 'seq', l: 'S.No', w: 38 },
    { k: 'raw_material', l: 'Material (T×W×L)', w: 200 },
    { k: 'uom', l: 'Qty UOM', w: 52 },
    { k: 'nesting_confirmed_qty', l: 'Nesting Qty', w: 118 },
    { k: 'processes', l: 'Processes', w: 120 },
  ];
  const rawColsSumW = rawColsTemplate.reduce((sum, c) => sum + (c.w || 0), 0) || 1;
  const rawColsScale = contentW / rawColsSumW;
  const rawCols = rawColsTemplate.map((c) => ({ ...c, w: Math.max(8, Math.round((c.w || 0) * rawColsScale)) }));
  const rawColsScaledSum = rawCols.reduce((sum, c) => sum + (c.w || 0), 0);
  rawCols[rawCols.length - 1].w += Math.round(contentW - rawColsScaledSum);
  y = drawTableHeader(y, rawCols);
  const rawRows = rawMaterialPdfRows.length > 0
    ? rawMaterialPdfRows
    : [{ seq: '-', raw_material: 'No raw materials found', uom: '-', nesting_confirmed_qty: '-', processes: '-' }];
  rawRows.forEach((r) => {
    const rowH = measureTableRowHeight(rawCols, r, 7.5);
    if (y + rowH > pageH - margin - 30) y = newPageForContinuation('RAW MATERIAL', rawCols);
    y = drawTableRow(y, rawCols, r, 7.5);
  });

  y += 12;
  if (y + 80 > pageH - margin - 30) {
    doc.addPage({ size: 'A4', margin: 24 });
    drawPageBox();
    y = margin;
    y = drawCompanyHeader(y);
    y = drawSectionTitle(y, 'ROUTE CARD - OPERATION DETAILS');
    y += 8;
  }
  doc.font('Helvetica-Bold').fontSize(11).text('OPERATION DETAILS', margin, y, { width: contentW, align: 'left' });
  y += 14;

  // Build column widths dynamically so the table fills `contentW` completely.
  // (Previously sum(w) < contentW left extra empty space after "End Date & Time".)
  // Also remove "Order Qty" and re-scale to prevent header/text overlap.
  const rawOpCols = [
    { k: 'seq', l: 'Seq', w: 28 },
    { k: 'op', l: 'Operation', w: 102 },
    { k: 'wc', l: 'Work Center', w: 82 },
    { k: 'released', l: 'Released Qty', w: 54 },
    { k: 'accepted', l: 'Accepted Qty', w: 58 },
    { k: 'rejected', l: 'Rejected Qty', w: 52 },
    { k: 'rejType', l: 'Rej Type', w: 46 },
    // Wider operator so names fit more comfortably.
    { k: 'operator', l: 'Operator', w: 126 },
    { k: 'start', l: 'Start Date & Time', w: 78 },
    { k: 'end', l: 'End Date & Time', w: 84 }
  ];
  const rawSumW = rawOpCols.reduce((sum, c) => sum + (c.w || 0), 0) || 1;
  const scaleW = contentW / rawSumW;
  const opCols = rawOpCols.map((c) => ({ ...c, w: Math.max(8, Math.round((c.w || 0) * scaleW)) }));
  const scaledSumW = opCols.reduce((sum, c) => sum + (c.w || 0), 0);
  // Fix any rounding difference by adjusting the last column.
  opCols[opCols.length - 1].w += Math.max(-50, Math.min(50, Math.round(contentW - scaledSumW)));
  y = drawTableHeader(y, opCols);

  const ops = (record.operations || []).map((o) => {
    const opName = oneLine(o.operation_name || '');
    const wcDesc = oneLine(o.workCenter?.description || '');
    return {
      seq: o.operation_sequence_no || '',
      op: opName || '—',
      wc: o.workCenter ? (wcDesc || '—') : '',
      released: fmtNum(o.input_qty),
      accepted: fmtNum(o.completed_qty),
      rejected: fmtNum(o.rejected_qty),
      rejType: [o.rejection_fr ? 'FR' : '', o.rejection_mr ? 'MR' : ''].filter(Boolean).join(', ') || '-',
      operator: o.operator?.operator_name || '',
      start: formatDateTime(o.actual_start),
      end: formatDateTime(o.actual_end)
    };
  });
  if (ops.length === 0) {
    ops.push({
      seq: '-',
      op: 'No operations',
      wc: '-',
      released: '-',
      accepted: '-',
      rejected: '-',
      rejType: '-',
      operator: '-',
      start: '-',
      end: '-'
    });
  }

  ops.forEach((r) => {
    const rowH = measureTableRowHeight(opCols, r, 7);
    if (y + rowH > pageH - margin - 20) y = newPageForContinuation('OPERATION DETAILS', opCols);
    y = drawTableRow(y, opCols, r, 7);
  });

  y = drawSignOffFooter(y);

  doc.end();
});

/**
 * Separate A4 "Process Log Sheet" PDF (does not alter the standard Route Card PDF).
 * Items default to materials assigned on Laser/LSR routing steps; falls back to all routing materials if none match.
 */
exports.generateProcessLogSheetPDF = asyncHandler(async (req, res) => {
  const { id } = req.params;

  const record = await db.RouteCard.findByPk(id, {
    include: [
      {
        model: db.WorkOrder,
        as: 'workOrder',
        attributes: [
          'id',
          'work_order_number',
          'work_order_quantity',
          'routing_id',
          'approved_by',
          'planned_start_date',
          'planned_end_date',
          'status',
          'createdAt',
          'updatedAt',
        ],
      },
      {
        model: db.ProductionOrder,
        as: 'productionOrder',
        attributes: ['id', 'production_order_number', 'customer_po_id', 'customer_po_item_id', 'order_quantity'],
        include: [
          {
            model: db.CustomerPO,
            as: 'customerPO',
            attributes: ['id', 'po_number', 'po_date', 'delivery_date'],
            include: [
              {
                model: db.BusinessPartner,
                as: 'customer',
                attributes: ['id', 'name', 'legal_name', 'bp_number'],
              },
            ],
          },
          {
            model: db.ProductionOrderComponent,
            as: 'components',
            attributes: ['id', 'required_quantity', 'material_id'],
            required: false,
            include: [
              { model: db.Material, as: 'material', attributes: ['id', 'material_number', 'description'], required: false },
            ],
          },
        ],
      },
      { model: db.Material, as: 'finishedMaterial', attributes: ['id', 'material_number', 'description', 'drawing_number', 'revision_number'] },
      {
        model: db.BOMHeader,
        as: 'bom',
        attributes: ['id', 'bom_number', 'drawing_number', 'revision_number', 'base_quantity'],
      },
      {
        model: db.RouteCardOperation,
        as: 'operations',
        attributes: ['id', 'operation_sequence_no', 'planned_qty', 'actual_start', 'actual_end', 'status'],
      },
    ],
    order: [[{ model: db.RouteCardOperation, as: 'operations' }, 'operation_sequence_no', 'ASC']],
  });

  if (!record) return sendError(res, 'Route Card not found', 404);

  /** DD/MM/YYYY — safe for MySQL DATEONLY strings (no timezone shift on YYYY-MM-DD). */
  const formatDateDMY = (v) => {
    if (v == null || v === '') return '';
    if (typeof v === 'string') {
      const m = v.trim().match(/^(\d{4})-(\d{2})-(\d{2})/);
      if (m) {
        const [, y, mo, da] = m;
        return `${da}/${mo}/${y}`;
      }
    }
    const d = v instanceof Date ? v : new Date(v);
    if (Number.isNaN(d.getTime())) return '';
    const day = String(d.getDate()).padStart(2, '0');
    const month = String(d.getMonth() + 1).padStart(2, '0');
    const year = d.getFullYear();
    return `${day}/${month}/${year}`;
  };

  let customerPO = record.productionOrder?.customerPO;
  const poHeadId = record.productionOrder?.customer_po_id;
  const poItemId = record.productionOrder?.customer_po_item_id;
  let poLineDeliveryDate = null;
  if (!customerPO && poHeadId) {
    customerPO = await db.CustomerPO.findByPk(poHeadId, {
      attributes: ['id', 'po_number', 'po_date', 'delivery_date'],
      include: [
        {
          model: db.BusinessPartner,
          as: 'customer',
          attributes: ['id', 'name', 'legal_name', 'bp_number'],
          required: false,
        },
      ],
    });
  }
  if (!customerPO && poItemId) {
    const cpi = await db.CustomerPOItem.findByPk(poItemId, {
      attributes: ['id', 'po_id', 'delivery_date'],
      include: [
        {
          model: db.CustomerPO,
          as: 'customerPO',
          attributes: ['id', 'po_number', 'po_date', 'delivery_date'],
          required: false,
          include: [
            {
              model: db.BusinessPartner,
              as: 'customer',
              attributes: ['id', 'name', 'legal_name', 'bp_number'],
              required: false,
            },
          ],
        },
      ],
    });
    customerPO = cpi?.customerPO || null;
    poLineDeliveryDate = cpi?.delivery_date || null;
  }
  if (
    poItemId &&
    customerPO &&
    (customerPO.delivery_date == null || customerPO.delivery_date === '')
  ) {
    const lineRow = await db.CustomerPOItem.findByPk(poItemId, { attributes: ['delivery_date'] });
    if (lineRow?.delivery_date != null && lineRow.delivery_date !== '') {
      poLineDeliveryDate = lineRow.delivery_date;
    }
  }

  const fmtNum = (v, decimals = 3) => {
    const n = parseFloat(v || 0);
    return Number.isFinite(n) ? n.toFixed(decimals) : '0.000';
  };

  const fmtQtyDisplay = (v) => {
    const n = parseFloat(v || 0);
    if (!Number.isFinite(n)) return '0';
    return Math.abs(n - Math.round(n)) < 1e-9 ? String(Math.round(n)) : fmtNum(n, 3);
  };

  const oneLine = (value) => String(value != null ? value : '').replace(/\s+/g, ' ').trim();

  const parseOperationMaterialIdsPls = (raw) => {
    if (!raw) return [];
    let parsed = raw;
    if (typeof parsed === 'string') {
      try {
        parsed = JSON.parse(parsed);
      } catch (e) {
        return [];
      }
    }
    if (!Array.isArray(parsed)) return [];
    const ids = [];
    parsed.forEach((entry) => {
      if (entry == null) return;
      if (Number.isFinite(parseInt(entry, 10))) {
        ids.push(parseInt(entry, 10));
        return;
      }
      if (typeof entry === 'object') {
        const candidate = entry.material_id || entry.materialId || entry.id || entry.value;
        if (Number.isFinite(parseInt(candidate, 10))) ids.push(parseInt(candidate, 10));
      }
    });
    return ids;
  };

  const isLaserRoutingStep = (op, wc) => {
    const blob = `${op?.operation_code || ''} ${op?.operation_description || ''} ${wc?.work_center_code || ''} ${wc?.description || ''}`.toUpperCase();
    if (/\b(TURRET|TPP|PUNCH)\b/.test(blob)) return false;
    return /\b(LASER|LSR)\b/.test(blob);
  };

  const classifyTimeBucket = (op, wc) => {
    const blob = `${op?.operation_code || ''} ${op?.operation_description || ''} ${wc?.work_center_code || ''} ${wc?.description || ''}`.toUpperCase();
    if (/\b(LASER|LSR)\b/.test(blob) && !/\bTURRET\b/.test(blob)) return 'LSR';
    if (/\b(TURRET|TPP|PUNCH)\b/.test(blob)) return 'TPP';
    if (/\b(BEN|BEND|PDL|DEBURR|DEBUR|FDR)\b/.test(blob)) return 'BEN';
    if (/\b(WELD)\b/.test(blob)) return 'WELDING';
    if (/\b(POWDER|COATING|PAINT)\b/.test(blob)) return 'POWDER';
    if (/\b(ASSEMBL)\b/.test(blob)) return 'ASSEMBLY';
    return 'OTHER';
  };

  const formatDurationFromSeconds = (totalSec) => {
    const s = Math.max(0, Math.round(totalSec));
    const h = Math.floor(s / 3600);
    const m = Math.floor((s % 3600) / 60);
    const sec = s % 60;
    return `${h}:${String(m).padStart(2, '0')}:${String(sec).padStart(2, '0')}`;
  };

  const printHeader = await db.PrintHeader.findOne({
    where: { company_id: record.company_id, status: 'ACTIVE' },
    order: [['id', 'DESC']],
  });

  const docNumberQuery = req.query?.documentNumber != null ? String(req.query.documentNumber).trim() : '';
  let processLogDocument = null;
  if (docNumberQuery) {
    processLogDocument = await db.Document.findOne({
      where: { company_id: record.company_id, status: 'ACTIVE', document_number: docNumberQuery },
    });
  }
  if (!processLogDocument) {
    processLogDocument = await db.Document.findOne({
      where: {
        company_id: record.company_id,
        status: 'ACTIVE',
        [Op.or]: [
          { title: { [Op.like]: '%Process Log%' } },
          { document_number: 'RI/MF/F/01' },
        ],
      },
      order: [['id', 'DESC']],
    });
  }

  const routingId = record.workOrder?.routing_id;
  const routingOps = routingId
    ? await db.RoutingOperation.findAll({
        where: { routing_id: routingId, status: 'ACTIVE' },
        attributes: [
          'id',
          'operation_number',
          'operation_code',
          'operation_description',
          'operation_materials_json',
          'machine_time',
          'labor_time',
          'base_quantity',
          'work_center_id',
        ],
        include: [{ model: db.WorkCenter, as: 'workCenter', attributes: ['work_center_code', 'description'], required: false }],
        order: [['operation_number', 'ASC'], ['id', 'ASC']],
      })
    : [];

  const poComponentQtyByMaterialId = new Map();
  (record?.productionOrder?.components || []).forEach((c) => {
    const key = String(c?.material_id || '');
    if (!key) return;
    const prev = poComponentQtyByMaterialId.get(key) || 0;
    poComponentQtyByMaterialId.set(key, prev + (parseFloat(c?.required_quantity || 0) || 0));
  });

  /** Per finished-good qty from routing operation_materials_json (qty / quantity). */
  const routingQtyPerSetByMaterialId = new Map();
  routingOps.forEach((op) => {
    const raw = op?.operation_materials_json;
    if (!raw) return;
    let arr = raw;
    if (typeof arr === 'string') {
      try {
        arr = JSON.parse(arr);
      } catch (e) {
        return;
      }
    }
    if (!Array.isArray(arr)) return;
    arr.forEach((entry) => {
      if (entry == null) return;
      if (typeof entry !== 'object') return;
      const mid = parseInt(entry.material_id || entry.materialId || entry.id || entry.value, 10);
      if (!Number.isFinite(mid) || mid <= 0) return;
      const qRaw = entry.qty ?? entry.quantity;
      if (qRaw === '' || qRaw == null) return;
      const n = parseFloat(qRaw);
      if (Number.isFinite(n) && n > 0) routingQtyPerSetByMaterialId.set(String(mid), n);
    });
  });

  const laserMaterialOrder = [];
  const materialLaserOpSeq = new Map();
  routingOps.forEach((op) => {
    if (!isLaserRoutingStep(op, op.workCenter)) return;
    parseOperationMaterialIdsPls(op.operation_materials_json).forEach((mid) => {
      const k = String(mid);
      if (!materialLaserOpSeq.has(k)) {
        materialLaserOpSeq.set(k, op.operation_number);
        laserMaterialOrder.push(mid);
      }
    });
  });

  const fallbackMaterialOrder = [];
  if (laserMaterialOrder.length === 0) {
    routingOps.forEach((op) => {
      parseOperationMaterialIdsPls(op.operation_materials_json).forEach((mid) => {
        const k = String(mid);
        if (!materialLaserOpSeq.has(k)) {
          materialLaserOpSeq.set(k, op.operation_number);
          fallbackMaterialOrder.push(mid);
        }
      });
    });
  }

  const orderedMaterialIds = laserMaterialOrder.length > 0 ? laserMaterialOrder : fallbackMaterialOrder;
  const uniqueIds = [...new Set(orderedMaterialIds.map((x) => parseInt(x, 10)).filter((n) => Number.isFinite(n) && n > 0))];

  const routingMaterials =
    uniqueIds.length > 0
      ? await db.Material.findAll({
          where: { id: { [Op.in]: uniqueIds } },
          attributes: [
            'id',
            'material_number',
            'description',
            'drawing_number',
            'revision_number',
            'length',
            'width',
            'height',
            'thickness',
          ],
        })
      : [];
  const routingMaterialMap = new Map(routingMaterials.map((m) => [String(m.id), m]));
  const routingOpByNumber = new Map(routingOps.map((o) => [Number(o.operation_number), o]));
  const classifyRoutingOpShort = (op) => {
    if (!op) return '—';
    const wc = op.workCenter;
    const blob = `${op.operation_code || ''} ${op.operation_description || ''} ${wc?.work_center_code || ''} ${wc?.description || ''}`.toUpperCase();
    if (/\b(BEND|BEN|SHEAR|SHR|PDL|DEBURR|DEBUR|FDR)\b/.test(blob)) return 'BEN';
    if (/\b(TURRET|TPP|PUNCH)\b/.test(blob)) return 'TPP';
    if (/\b(LASER|LSR)\b/.test(blob)) return 'LSR';
    return '—';
  };

  const woQty = parseFloat(record.workOrder?.work_order_quantity || 0) || 0;
  const poQty = parseFloat(record.productionOrder?.order_quantity || 0) || 0;

  const bomId = record.bom_id;
  const bomBaseQty = parseFloat(record.bom?.base_quantity ?? 1) || 1;
  const bomComps =
    bomId && uniqueIds.length > 0
      ? await db.BOMComponent.findAll({
          where: {
            bom_id: bomId,
            status: 'ACTIVE',
            material_id: { [Op.in]: uniqueIds },
          },
          attributes: ['material_id', 'component_quantity'],
        })
      : [];
  const bomQtyPerBaseByMaterialId = new Map();
  bomComps.forEach((bc) => {
    const mid = String(bc.material_id);
    const prev = bomQtyPerBaseByMaterialId.get(mid) || 0;
    bomQtyPerBaseByMaterialId.set(mid, prev + (parseFloat(bc.component_quantity || 0) || 0));
  });

  const rcOpBySeq = new Map();
  (record.operations || []).forEach((o) => {
    rcOpBySeq.set(Number(o.operation_sequence_no), o);
  });

  const signOffs = parsePdfSignOffsJson(record.pdf_sign_offs);
  const cid = record.company_id;
  const [verifiedByPls, qcInchargePls] = await Promise.all([
    formatUserNamesFromIds(signOffs.verified_by_user_ids, cid),
    formatUserNamesFromIds(signOffs.qc_incharge_user_ids, cid),
  ]);

  const woApprovedName = await (async () => {
    const aid = record.workOrder?.approved_by;
    if (!aid) return '';
    const u = await db.User.findByPk(aid, { attributes: ['username', 'first_name', 'last_name'] });
    if (!u) return '';
    const full = `${u.first_name || ''} ${u.last_name || ''}`.trim();
    return full || u.username || '';
  })();

  const timeBucketsSec = { LSR: 0, TPP: 0, BEN: 0, WELDING: 0, POWDER: 0, ASSEMBLY: 0, OTHER: 0 };
  /** Use completed operation actual start/end duration for process-log timing. */
  (record.operations || []).forEach((rcOp) => {
    if (String(rcOp?.status || '').toUpperCase() !== 'COMPLETED') return;
    if (!rcOp?.actual_start || !rcOp?.actual_end) return;
    const startMs = new Date(rcOp.actual_start).getTime();
    const endMs = new Date(rcOp.actual_end).getTime();
    if (!Number.isFinite(startMs) || !Number.isFinite(endMs) || endMs <= startMs) return;
    const sec = Math.round((endMs - startMs) / 1000);
    if (sec <= 0) return;
    const rOp = routingOpByNumber.get(Number(rcOp.operation_sequence_no));
    const bucket = classifyTimeBucket(rOp || {}, rOp?.workCenter);
    timeBucketsSec[bucket] = (timeBucketsSec[bucket] || 0) + sec;
  });

  const poCustomer = customerPO?.customer;
  const customerName = poCustomer?.name || poCustomer?.legal_name || '';

  const drawingNo = oneLine(record.bom?.drawing_number || record.finishedMaterial?.drawing_number || '');
  let itemDesc = oneLine(record.finishedMaterial?.description || '');
  if (drawingNo && itemDesc) {
    const escaped = drawingNo.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
    itemDesc = itemDesc.replace(new RegExp(`^${escaped}\\s*[-–—]?\\s*`, 'i'), '').trim();
  }
  const productDrawing = [itemDesc, drawingNo].filter(Boolean).join(' / ') || '—';

  const poNo = customerPO?.po_number || '';
  const poDate = formatDateDMY(customerPO?.po_date);
  const commitmentDate = formatDateDMY(customerPO?.delivery_date || poLineDeliveryDate);
  const customerCode = oneLine(poCustomer?.bp_number || '');
  const woNo = record.workOrder?.work_order_number || '';
  const woPlain = record.workOrder && typeof record.workOrder.toJSON === 'function' ? record.workOrder.toJSON() : record.workOrder;
  /** Work order "release" date: first Route Card on this WO (created when WO is released in standard flow). */
  const firstRcForWo = record.work_order_id
    ? await db.RouteCard.findOne({
        where: { work_order_id: record.work_order_id },
        attributes: ['id', 'start_date', 'createdAt', 'created_at'],
        order: [['id', 'ASC']],
      })
    : null;
  const firstRcJson = firstRcForWo && typeof firstRcForWo.toJSON === 'function' ? firstRcForWo.toJSON() : firstRcForWo;
  const woReleaseRaw =
    firstRcJson?.createdAt ||
    firstRcJson?.created_at ||
    firstRcJson?.start_date ||
    (String(woPlain?.status || '').toUpperCase() === 'RELEASED'
      ? woPlain?.updatedAt || woPlain?.updated_at
      : null) ||
    woPlain?.createdAt ||
    woPlain?.created_at;
  const woDate = formatDateDMY(woReleaseRaw);
  const docRef = processLogDocument?.document_number || '—';

  const tableRows = [];
  const validMids = orderedMaterialIds.filter((mid) => routingMaterialMap.get(String(mid)));
  const partLineCount = Math.max(validMids.length, 1);

  validMids.forEach((mid, idx) => {
    const mat = routingMaterialMap.get(String(mid));
    if (!mat) return;
    const midStr = String(mid);
    let qtyPerSet = null;
    const routeQ = routingQtyPerSetByMaterialId.get(midStr);
    if (routeQ != null && Number.isFinite(routeQ) && routeQ > 0) {
      qtyPerSet = routeQ;
    } else {
      const bomQ = bomQtyPerBaseByMaterialId.get(midStr);
      if (bomQ != null && bomBaseQty > 0) {
        qtyPerSet = bomQ / bomBaseQty;
      }
    }
    if (qtyPerSet == null || !Number.isFinite(qtyPerSet)) {
      const reqForPo = poComponentQtyByMaterialId.get(midStr) || 0;
      qtyPerSet = poQty > 0 ? reqForPo / poQty : reqForPo;
    }
    if (!Number.isFinite(qtyPerSet)) qtyPerSet = 0;
    const totalReq = qtyPerSet * woQty;
    const opSeq = materialLaserOpSeq.get(midStr);
    const rcOp = opSeq != null ? rcOpBySeq.get(Number(opSeq)) : null;
    const opPlannedBase =
      rcOp != null ? parseFloat(rcOp.planned_qty != null ? rcOp.planned_qty : woQty) || woQty : woQty;
    const planned = qtyPerSet * opPlannedBase;
    const matDwg = oneLine(mat.drawing_number || '');
    const itemCodeFromDrawing = matDwg || oneLine(mat.material_number || '') || '—';
    const ro = opSeq != null ? routingOpByNumber.get(Number(opSeq)) : null;
    const bomAppLevel =
      oneLine(mat.revision_number || '') ||
      oneLine(record.bom?.revision_number || '') ||
      '—';
    tableRows.push({
      itemCode: itemCodeFromDrawing,
      itemName: oneLine(mat.description || ''),
      bomAppLevel,
      drawingSheet: `${idx + 1} OF ${partLineCount}`,
      thick: mat.thickness != null ? fmtNum(mat.thickness, 2) : '—',
      length: mat.length != null ? fmtNum(mat.length, 2) : '—',
      width: mat.width != null ? fmtNum(mat.width, 2) : '—',
      qtySet: fmtQtyDisplay(qtyPerSet),
      totalReq: fmtQtyDisplay(totalReq),
      stockQty: '—',
      planned: fmtQtyDisplay(planned),
      lsrTppShr: classifyRoutingOpShort(ro),
      pbrStatus: '—',
      totalStrokes: '—',
      coating: '—',
    });
  });

  if (tableRows.length === 0) {
    tableRows.push({
      itemCode: '—',
      itemName: 'No materials on Laser/LSR routing step (and no other routing materials)',
      bomAppLevel: '—',
      drawingSheet: '—',
      thick: '—',
      length: '—',
      width: '—',
      qtySet: '—',
      totalReq: '—',
      stockQty: '—',
      planned: '—',
      lsrTppShr: '—',
      pbrStatus: '—',
      totalStrokes: '—',
      coating: '—',
    });
  }

  const safeRc = String(record.route_card_number || record.id).replace(/[<>:"/\\|?*\x00-\x1F]/g, '');
  const filename = `ProcessLogSheet_${safeRc}.pdf`;

  /** A4 portrait (points): width × height */
  const pageOpts = { size: [595, 842], margin: 18 };
  const doc = new PDFDocument(pageOpts);
  res.setHeader('Content-Type', 'application/pdf');
  res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
  doc.pipe(res);

  const margin = 18;
  const contentW = () => doc.page.width - margin * 2;

  const resolveLogoPathPls = (logoField) => {
    if (!logoField) return null;
    let fullPath = path.join(__dirname, '..', 'uploads', logoField);
    if (fs.existsSync(fullPath)) return fullPath;
    fullPath = path.join(__dirname, '..', logoField);
    if (fs.existsSync(fullPath)) return fullPath;
    return null;
  };
  const companyLogoPath = resolveLogoPathPls(printHeader?.logo_path);
  const companyTitle = (printHeader?.company_name || 'RAM INDUSTRIES').toUpperCase();
  const subtitle = /\bCOIMBATORE\b/i.test(String(printHeader?.address || ''))
    ? 'COIMBATORE'
    : oneLine(String(printHeader?.address || '').split(/\r?\n/)[0] || 'COIMBATORE');
  const brandLine = `${companyTitle} ${subtitle}`.trim();

  /** Reference sheet: sans-serif (Helvetica), minimal fill. */
  const F = 'Helvetica';
  const FB = 'Helvetica-Bold';
  const hdrFill = '#ffffff';
  const hdrText = '#000000';

  const fitOneLine = (text, maxWidth, fontName = F, fontSize = 7) => {
    let out = oneLine(text || '') || '—';
    doc.font(fontName).fontSize(fontSize);
    while (out.length > 1 && doc.widthOfString(out) > maxWidth) out = out.slice(0, -1);
    return out;
  };

  const cols = [
    { k: 'itemCode', l: 'ITEM CODE', w: 0.09 },
    { k: 'itemName', l: 'ITEM NAME', w: 0.14 },
    { k: 'bomAppLevel', l: 'BOM/\nAPP LEVEL', w: 0.055 },
    { k: 'drawingSheet', l: 'DRAWING\nSHEET NO', w: 0.075 },
    { k: 'thick', l: 'THICK\n(IN MM)', w: 0.06 },
    { k: 'length', l: 'LENGTH\n(IN MM)', w: 0.065 },
    { k: 'width', l: 'WIDTH\n(IN MM)', w: 0.065 },
    { k: 'qtySet', l: 'QTY /\nSET', w: 0.055 },
    { k: 'totalReq', l: 'TOTAL REQ\nQTY', w: 0.065 },
    { k: 'stockQty', l: 'STOCK\nQTY', w: 0.055 },
    { k: 'planned', l: 'PLANNED\nQTY', w: 0.065 },
    { k: 'lsrTppShr', l: 'LSR\nTPP BEN', w: 0.065 },
    { k: 'pbrStatus', l: 'BEN\nSTATUS', w: 0.055 },
    { k: 'totalStrokes', l: 'TOTAL\nSTROKES', w: 0.06 },
    { k: 'coating', l: 'COATING\nCOLOUR', w: 0.065 },
  ];
  const colWidthsFor = (cw) => {
    const ws = cols.map((c) => Math.max(16, Math.floor(cw * c.w)));
    const sum = ws.reduce((a, b) => a + b, 0);
    ws[ws.length - 1] += Math.round(cw - sum);
    return ws;
  };

  const drawTableHeaderPls = (yPos, cw) => {
    const colWidths = colWidthsFor(cw);
    const headerHLocal = 34;
    const yBot = yPos + headerHLocal;
    doc.save();
    doc.rect(margin, yPos, cw, headerHLocal).fill(hdrFill);
    doc.restore();
    doc.moveTo(margin, yPos).lineTo(margin, yBot).stroke('#000');
    doc.moveTo(margin + cw, yPos).lineTo(margin + cw, yBot).stroke('#000');
    doc.moveTo(margin, yBot).lineTo(margin + cw, yBot).stroke('#000');
    let x = margin;
    cols.forEach((c, i) => {
      x += colWidths[i];
      if (x < margin + cw) doc.moveTo(x, yPos).lineTo(x, yBot).stroke('#000');
    });
    x = margin;
    cols.forEach((c, i) => {
      doc.font(FB).fontSize(5.4).fillColor(hdrText).text(c.l, x + 2, yPos + 5, {
        width: Math.max(4, colWidths[i] - 4),
        lineGap: 0.5,
        align: 'center',
      });
      x += colWidths[i];
    });
    doc.fillColor('#000');
    return { y: yPos + headerHLocal, colWidths };
  };

  const rowHeightPls = (row, colWidths) => {
    doc.font(F).fontSize(6.5);
    let maxH = 14;
    let x = margin;
    cols.forEach((c, i) => {
      const text = String(row[c.k] || '');
      const h = doc.heightOfString(text, { width: Math.max(4, colWidths[i] - 4), lineGap: 0.5 });
      maxH = Math.max(maxH, h + 8);
      x += colWidths[i];
    });
    return Math.max(maxH, 16);
  };

  const drawRowPls = (yPos, row, colWidths) => {
    const cw = colWidths.reduce((a, b) => a + b, 0);
    const rh = rowHeightPls(row, colWidths);
    const yBot = yPos + rh;
    doc.moveTo(margin, yPos).lineTo(margin, yBot).stroke('#000');
    doc.moveTo(margin + cw, yPos).lineTo(margin + cw, yBot).stroke('#000');
    doc.moveTo(margin, yBot).lineTo(margin + cw, yBot).stroke('#000');
    let x = margin;
    cols.forEach((c, i) => {
      const text = String(row[c.k] || '');
      const align = i <= 2 ? 'left' : 'center';
      doc.font(F).fontSize(6.5).fillColor('#000').text(text, x + 2, yPos + 4, {
        width: Math.max(4, colWidths[i] - 4),
        lineGap: 0.5,
        align,
      });
      x += colWidths[i];
      if (x < margin + cw) doc.moveTo(x, yPos).lineTo(x, yBot).stroke('#000');
    });
    return yPos + rh;
  };

  /**
   * Image-2 style: [left: company+PO] | [center: title + 3×4 grid] | [right: doc ref only + customer];
   * No fill colors; black borders only. Document number only (no label). No route card line.
   */
  const drawPageHeader = (yStart) => {
    const cw = contentW();
    const leftW = Math.round(cw * 0.24);
    const rightW = Math.min(100, Math.round(cw * 0.2));
    const centerW = cw - leftW - rightW;
    const totalH = 138;
    const titleH = 36;
    const docTopH = 16;
    const gridH = totalH - titleH;
    const rowH = gridH / 3;
    const x0 = margin;
    const y0 = yStart;
    const xMid = x0 + leftW;
    const xRight = x0 + leftW + centerW;

    const leftUpperH = 0.48 * totalH;
    const yPo0 = y0 + leftUpperH;
    const poRowH = (totalH - leftUpperH) / 2;

    doc.rect(x0, y0, cw, totalH).stroke('#000');
    doc.moveTo(xMid, y0).lineTo(xMid, y0 + totalH).stroke('#000');
    doc.moveTo(xRight, y0).lineTo(xRight, y0 + totalH).stroke('#000');
    doc.moveTo(x0, yPo0).lineTo(xMid, yPo0).stroke('#000');
    doc.moveTo(x0, yPo0 + poRowH).lineTo(xMid, yPo0 + poRowH).stroke('#000');
    // Title/meta split continues through right column like reference header.
    doc.moveTo(xMid, y0 + titleH).lineTo(x0 + cw, y0 + titleH).stroke('#000');
    // Small top strip in right column for document number only.
    doc.moveTo(xRight, y0 + docTopH).lineTo(x0 + cw, y0 + docTopH).stroke('#000');

    for (let r = 1; r <= 3; r += 1) {
      doc.moveTo(xMid, y0 + titleH + r * rowH).lineTo(x0 + cw, y0 + titleH + r * rowH).stroke('#000');
    }

    const w1 = Math.floor(centerW * 0.19);
    const w2 = Math.floor(centerW * 0.38);
    const w3 = Math.floor(centerW * 0.15);
    const w4 = centerW - w1 - w2 - w3;
    const gx1 = xMid + w1;
    const gx2 = gx1 + w2;
    const gx3 = gx2 + w3;
    const yGrid = y0 + titleH;
    doc.moveTo(gx1, yGrid).lineTo(gx1, y0 + totalH).stroke('#000');
    doc.moveTo(gx2, yGrid).lineTo(gx2, y0 + totalH).stroke('#000');
    doc.moveTo(gx3, yGrid).lineTo(gx3, y0 + totalH).stroke('#000');

    const logoBox = 28;
    const logoX = x0 + 4;
    const logoY = y0 + (leftUpperH - logoBox) / 2;
    if (companyLogoPath) {
      try {
        doc.image(companyLogoPath, logoX, logoY, { fit: [logoBox, logoBox] });
      } catch (e) {
        /* ignore */
      }
    }
    const brandX = logoX + logoBox + 4;
    doc.font(FB).fontSize(8).fillColor('#000').text(brandLine, brandX, y0 + leftUpperH / 2 - 6, {
      width: xMid - brandX - 4,
      lineGap: 1,
    });

    const poLabelW = 30;
    const poValueW = leftW - 8 - poLabelW;
    const poNoText = fitOneLine(poNo || '—', poValueW, F, 6.8);
    const poDateText = fitOneLine(poDate || '—', poValueW, F, 6.8);
    doc.font(FB).fontSize(6).text('PO No :-', x0 + 4, yPo0 + 9, { width: poLabelW, lineBreak: false });
    doc.font(F).fontSize(6.8).text(poNoText, x0 + 4 + poLabelW, yPo0 + 9, { width: poValueW, lineBreak: false });
    doc.font(FB).fontSize(6).text('PO Date :-', x0 + 4, yPo0 + poRowH + 9, { width: poLabelW, lineBreak: false });
    doc.font(F).fontSize(6.8).text(poDateText, x0 + 4 + poLabelW, yPo0 + poRowH + 9, { width: poValueW, lineBreak: false });

    doc.font(FB).fontSize(13).text('PROCESS LOG SHEET', xMid, y0 + 10, {
      width: centerW,
      align: 'center',
    });

    doc.font(FB).fontSize(9).fillColor('#000').text(docRef, xRight + 4, y0 + 4, { width: rightW - 8, align: 'right' });
    doc.font(FB).fontSize(6).text('CUSTOMER NAME', xRight + 4, yGrid + 4, { width: rightW - 8 });
    doc.font(F).fontSize(6.5).text(oneLine(customerName) || '—', xRight + 4, yGrid + 12, { width: rightW - 8 });
    doc.font(FB).fontSize(6).text('CUSTOMER CODE', xRight + 4, yGrid + rowH + 4, { width: rightW - 8 });
    doc.font(F).fontSize(7.5).text(customerCode || '—', xRight + 4, yGrid + rowH + 14, { width: rightW - 8 });

    let yy = yGrid;
    doc.font(FB).fontSize(5.8).text('WORK ORDER NO :', xMid + 2, yy + 8, { width: w1 - 4 });
    doc.font(F).fontSize(6.5).text(woNo || '—', gx1 + 2, yy + 8, { width: w2 - 4 });
    doc.font(FB).fontSize(5.8).text('DATE :', gx2 + 2, yy + 8, { width: w3 - 4 });
    doc.font(F).fontSize(6.5).text(woDate || '—', gx3 + 2, yy + 8, { width: w4 - 4 });

    yy += rowH;
    doc.font(FB).fontSize(5.5).text('PRODUCT / DRAWING NO :', xMid + 2, yy + 8, { width: w1 - 4 });
    doc.font(F).fontSize(6).text(productDrawing, gx1 + 2, yy + 8, { width: w2 - 4, lineGap: 0.5 });
    doc.font(FB).fontSize(5.8).text('PO QTY :', gx2 + 2, yy + 8, { width: w3 - 4 });
    doc.font(F).fontSize(6.5).text(fmtQtyDisplay(poQty), gx3 + 2, yy + 8, { width: w4 - 4 });

    yy += rowH;
    doc.font(FB).fontSize(5.8).text('COMMITMENT DATE :', xMid + 2, yy + 8, { width: w1 - 4 });
    doc.font(F).fontSize(6.5).text(commitmentDate || '—', gx1 + 2, yy + 8, { width: w2 - 4 });
    doc.font(FB).fontSize(5.8).text('WO QTY :', gx2 + 2, yy + 8, { width: w3 - 4 });
    doc.font(F).fontSize(6.5).text(fmtQtyDisplay(woQty), gx3 + 2, yy + 8, { width: w4 - 4 });

    doc.fillColor('#000');
    return y0 + totalH;
  };

  const drawContinuationHeader = (yStart) => {
    const cw = contentW();
    const h = 20;
    doc.rect(margin, yStart, cw, h).stroke('#000');
    doc.font(FB).fontSize(8).text('PROCESS LOG SHEET (continued)', margin + 6, yStart + 7, { width: cw - 12 });
    doc.fillColor('#000');
    return yStart + h + 6;
  };

  const drawTimeBlock = (y0, cw, colWidths) => {
    const timeLabels = [
      ['LSR TOTAL TIME', 'LSR'],
      ['TPP TOTAL TIME', 'TPP'],
      ['BEN TOTAL TIME', 'BEN'],
      ['WELDING TOTAL TIMING', 'WELDING'],
      ['POWDER COATING TIMING', 'POWDER'],
      ['ASSEMBLY TIMING', 'ASSEMBLY'],
    ];
    const timeRowH = 16;
    const totalH = timeRowH * 2;
    const getBreakX = (idx) => {
      if (!Array.isArray(colWidths) || colWidths.length === 0) return null;
      let x = margin;
      for (let i = 0; i <= idx && i < colWidths.length; i += 1) x += colWidths[i];
      return x;
    };
    const x1Aligned = getBreakX(3); // Right edge of DRAWING SHEET NO
    const x2Aligned = getBreakX(8); // Right edge of TOTAL REQ QTY
    const y1 = y0 + timeRowH;
    const yb = y0 + totalH;
    const x1 = x1Aligned != null ? x1Aligned : margin + (cw / 3);
    const x2 = x2Aligned != null ? x2Aligned : margin + (2 * cw / 3);
    const widths = [x1 - margin, x2 - x1, margin + cw - x2];
    doc.moveTo(margin, y0).lineTo(margin, yb).stroke('#000');
    doc.moveTo(margin + cw, y0).lineTo(margin + cw, yb).stroke('#000');
    doc.moveTo(margin, yb).lineTo(margin + cw, yb).stroke('#000');
    doc.moveTo(margin, y1).lineTo(margin + cw, y1).stroke('#000');
    doc.moveTo(x1, y0).lineTo(x1, yb).stroke('#000');
    doc.moveTo(x2, y0).lineTo(x2, yb).stroke('#000');
    let y = y0;
    let tx = margin;
    for (let i = 0; i < 3; i += 1) {
      const [label, key] = timeLabels[i];
      const w = widths[i];
      doc.font(FB).fontSize(6).fillColor('#000').text(label, tx + 3, y + 4, { width: w - 8, lineBreak: false });
      doc.font(F).fontSize(7).text(formatDurationFromSeconds(timeBucketsSec[key]), tx + 3, y + 4, {
        width: w - 8,
        align: 'right',
        lineBreak: false,
      });
      tx += w;
    }
    y += timeRowH;
    tx = margin;
    for (let i = 0; i < 3; i += 1) {
      const [label, key] = timeLabels[i + 3];
      const w = widths[i];
      doc.font(FB).fontSize(6).fillColor('#000').text(label, tx + 3, y + 4, { width: w - 8, lineBreak: false });
      doc.font(F).fontSize(7).text(formatDurationFromSeconds(timeBucketsSec[key]), tx + 3, y + 4, {
        width: w - 8,
        align: 'right',
        lineBreak: false,
      });
      tx += w;
    }
    y = yb;
    if (timeBucketsSec.OTHER > 0) {
      const oh = 14;
      doc.rect(margin, y, cw, oh).stroke('#000');
      doc.font(F).fontSize(6.5).text(
        `OTHER: ${formatDurationFromSeconds(timeBucketsSec.OTHER)}`,
        margin + 4,
        y + 3,
        { width: cw - 8 }
      );
      y += oh;
    }
    return y;
  };

  const drawSignRow = (y0, cw) => {
    const signH = 42;
    const blankH = 20;
    const titles = [
      'CARD OPENING\nAPPROVED BY',
      'LOT-I',
      'LOT-II',
      'LOT-III',
      'LOT-IV',
      'QC SIGN',
      'CARD CLOSING\nAPPROVED BY',
    ];
    const wCol = cw / 7;
    const yb = y0 + signH;
    doc.moveTo(margin, y0).lineTo(margin, yb).stroke('#000');
    doc.moveTo(margin + cw, y0).lineTo(margin + cw, yb).stroke('#000');
    doc.moveTo(margin, yb).lineTo(margin + cw, yb).stroke('#000');
    titles.forEach((t, i) => {
      const sx = margin + i * wCol;
      if (i < 6) doc.moveTo(sx + wCol, y0).lineTo(sx + wCol, yb).stroke('#000');
      doc.font(FB).fontSize(5.5).fillColor('#000').text(t, sx + 2, y0 + 4, {
        width: wCol - 4,
        align: 'center',
        lineGap: 1,
      });
      // Names/sign text is printed in the bottom row (as in reference format).
    });
    const y2 = yb + blankH;
    doc.moveTo(margin, yb).lineTo(margin, y2).stroke('#000');
    doc.moveTo(margin + cw, yb).lineTo(margin + cw, y2).stroke('#000');
    doc.moveTo(margin, y2).lineTo(margin + cw, y2).stroke('#000');
    for (let i = 0; i < 6; i += 1) {
      const sx = margin + (i + 1) * wCol;
      doc.moveTo(sx, yb).lineTo(sx, y2).stroke('#000');
    }
    doc.font(F).fontSize(6.5).text(verifiedByPls || woApprovedName || ' ', margin + 2, yb + 5, {
      width: wCol - 4,
      align: 'center',
    });
    doc.font(F).fontSize(6.5).text(qcInchargePls || ' ', margin + (5 * wCol) + 2, yb + 5, {
      width: wCol - 4,
      align: 'center',
    });
    return y2;
  };

  /**
   * Route-card style multi-section log page (as per attached shop-floor sheet).
   * This page is intentionally mostly blank grids for manual entries.
   */
  const drawRouteCardLogFormatPage = () => {
    const cw = contentW();
    const x0 = margin;
    let y = margin;

    const strokeBox = (x, yPos, w, h) => {
      doc.rect(x, yPos, w, h).stroke('#000');
    };

    const drawGrid = ({
      title,
      columns,
      rows,
      yStart,
      headerH = 16,
      rowH = 14,
      titleH = 14,
      fontSize = 6.2,
      titleFontSize = 7.2,
    }) => {
      const w = cw;
      const h = titleH + headerH + rows * rowH;
      strokeBox(x0, yStart, w, h);

      // Title band
      doc.font(FB).fontSize(titleFontSize).text(title, x0, yStart + 3, { width: w, align: 'center' });
      doc.moveTo(x0, yStart + titleH).lineTo(x0 + w, yStart + titleH).stroke('#000');

      // Header row
      const yHeaderTop = yStart + titleH;
      const yHeaderBot = yHeaderTop + headerH;
      doc.moveTo(x0, yHeaderBot).lineTo(x0 + w, yHeaderBot).stroke('#000');

      // Column verticals + header text
      let xx = x0;
      const usableW = w;
      const widths = columns.map((c) => Math.max(18, Math.floor(usableW * (c.w || 0))));
      const sum = widths.reduce((a, b) => a + b, 0);
      widths[widths.length - 1] += usableW - sum;

      const fitHeader = (label, cellW) => {
        const txt = String(label || '');
        const maxW = Math.max(1, cellW - 6);
        let fs = fontSize;
        doc.font(FB).fontSize(fs);
        while (fs > 4.6 && doc.widthOfString(txt) > maxW) {
          fs -= 0.2;
          doc.font(FB).fontSize(fs);
        }
        if (doc.widthOfString(txt) <= maxW) return { text: txt, fs };
        // Still too long: truncate
        let out = txt;
        while (out.length > 1 && doc.widthOfString(`${out}…`) > maxW) out = out.slice(0, -1);
        return { text: `${out}…`, fs };
      };

      columns.forEach((c, i) => {
        const ww = widths[i];
        const fitted = fitHeader(c.label, ww);
        doc.font(FB).fontSize(fitted.fs).text(fitted.text, xx + 2, yHeaderTop + 4, {
          width: Math.max(4, ww - 4),
          align: c.align || 'center',
          lineBreak: false,
        });
        xx += ww;
        if (i < columns.length - 1) doc.moveTo(xx, yHeaderTop).lineTo(xx, yStart + h).stroke('#000');
      });

      // Horizontal row lines (blank rows)
      for (let r = 0; r < rows; r += 1) {
        const yy = yHeaderBot + (r + 1) * rowH;
        doc.moveTo(x0, yy).lineTo(x0 + w, yy).stroke('#000');
      }

      return yStart + h;
    };

    // Top: TURRET + LASER — weights must sum to 1.0 so REMARKS is not shrunk by width correction.
    const topCols = [
      { label: 'SN', w: 0.04 },
      { label: 'DATE', w: 0.07 },
      { label: 'PROG NO', w: 0.085 },
      { label: 'THICKNESS', w: 0.07 },
      { label: 'SHEET SIZE', w: 0.07 },
      { label: 'QTY', w: 0.045 },
      { label: 'START TIME', w: 0.07 },
      { label: 'END TIME', w: 0.07 },
      { label: 'TOTAL TIME', w: 0.07 },
      { label: 'OPERATOR', w: 0.085 },
      { label: 'QC', w: 0.038 },
      { label: 'REMARKS', w: 0.287, align: 'left' },
    ];
    y = drawGrid({ title: 'TURRET PUNCH PRESS', columns: topCols, rows: 6, yStart: y, headerH: 14, rowH: 13, titleH: 13 });
    y += 6;
    y = drawGrid({ title: 'LASER', columns: topCols, rows: 6, yStart: y, headerH: 14, rowH: 13, titleH: 13 });
    y += 6;

    // BENDING — same: weights sum to 1.0; ENGINEER → THICKNESS; OK → QC.
    const bendCols = [
      { label: 'SN', w: 0.04 },
      { label: 'DATE', w: 0.08 },
      { label: 'THICKNESS', w: 0.08 },
      { label: 'START TIME', w: 0.075 },
      { label: 'END TIME', w: 0.075 },
      { label: 'TOTAL TIME', w: 0.075 },
      { label: 'NO OF STROKE', w: 0.10 },
      { label: 'OPERATOR', w: 0.10 },
      { label: 'QC', w: 0.04 },
      { label: 'QTY CHECKED', w: 0.08 },
      { label: 'REMARKS', w: 0.255, align: 'left' },
    ];
    y = drawGrid({ title: 'BENDING', columns: bendCols, rows: 10, yStart: y, headerH: 14, rowH: 12, titleH: 13 });
    y += 6;

    // FABRICATION
    const fabCols = [
      { label: 'SN', w: 0.05 },
      { label: 'DATE', w: 0.10 },
      { label: 'WELDER NAME', w: 0.18, align: 'left' },
      { label: 'QTY', w: 0.10 },
      { label: 'WORK STARTING DATE', w: 0.18 },
      { label: 'WORK ENDING DATE', w: 0.18 },
      { label: 'TOTAL DAYS', w: 0.09 },
      { label: 'WELDER SIGNATURE', w: 0.12, align: 'left' },
    ];
    y = drawGrid({ title: 'FABRICATION', columns: fabCols, rows: 3, yStart: y, headerH: 14, rowH: 14, titleH: 13 });
    y += 6;

    // POWDER COATING
    const pcCols = [
      { label: 'SN', w: 0.05 },
      { label: 'DC NO', w: 0.10 },
      { label: 'DATE', w: 0.10 },
      { label: 'ITEM', w: 0.20, align: 'left' },
      { label: 'QTY', w: 0.07 },
      { label: 'PARTY NAME', w: 0.18, align: 'left' },
      { label: 'WORK STARTING DATE', w: 0.15 },
      { label: 'WORK ENDING DATE', w: 0.15 },
    ];
    y = drawGrid({ title: 'POWDER COATING', columns: pcCols, rows: 3, yStart: y, headerH: 14, rowH: 14, titleH: 13 });
    y += 6;

    // ASSY
    const assyCols = [
      { label: 'SN', w: 0.05 },
      { label: 'DATE', w: 0.10 },
      { label: 'NAME', w: 0.22, align: 'left' },
      { label: 'QTY', w: 0.10 },
      { label: 'WORK STARTING DATE', w: 0.18 },
      { label: 'WORK ENDING DATE', w: 0.18 },
      { label: 'TOTAL DAYS', w: 0.08 },
      { label: 'SIGNATURE', w: 0.07, align: 'left' },
    ];
    y = drawGrid({ title: 'ASSY', columns: assyCols, rows: 4, yStart: y, headerH: 14, rowH: 13, titleH: 13 });
    y += 6;

    // DESPATCH
    const desCols = [
      { label: 'INV NO', w: 0.16 },
      { label: 'INV DATE', w: 0.16 },
      { label: 'INV QTY', w: 0.14 },
      { label: 'LOADED BY', w: 0.18, align: 'left' },
      { label: 'CHECKED BY', w: 0.18, align: 'left' },
      { label: 'APPROVED BY', w: 0.18, align: 'left' },
    ];
    y = drawGrid({ title: 'DESPATCH', columns: desCols, rows: 1, yStart: y, headerH: 14, rowH: 16, titleH: 13 });
    y += 6;

    // ACCOUNTS
    const accCols = [
      { label: 'PO NO', w: 0.20 },
      { label: 'INV NO', w: 0.20 },
      { label: 'INV DATE', w: 0.20 },
      { label: 'INV QTY', w: 0.20 },
    ];
    y = drawGrid({ title: 'ACCOUNTS', columns: accCols, rows: 1, yStart: y, headerH: 14, rowH: 16, titleH: 13 });

    return y;
  };

  const footerReserve = 120;
  const drawFullTable = (startY) => {
    let y = startY;
    let cw = contentW();
    let hdr = drawTableHeaderPls(y, cw);
    y = hdr.y;
    let activeColWidths = hdr.colWidths;
    tableRows.forEach((row) => {
      const rh = rowHeightPls(row, activeColWidths);
      const pageH = doc.page.height;
      if (y + rh > pageH - margin - footerReserve) {
        doc.addPage(pageOpts);
        y = margin;
        y = drawContinuationHeader(y);
        cw = contentW();
        const h2 = drawTableHeaderPls(y, cw);
        y = h2.y;
        activeColWidths = h2.colWidths;
      }
      y = drawRowPls(y, row, activeColWidths);
    });
    return { y, colWidths: activeColWidths };
  };

  let y = margin;
  y = drawPageHeader(y);
  const tableResult = drawFullTable(y);
  y = tableResult.y;
  if (y + 80 > doc.page.height - margin) {
    doc.addPage(pageOpts);
    y = margin;
  }
  y = drawTimeBlock(y, contentW(), tableResult.colWidths);
  y = drawSignRow(y, contentW());

  // Second page: route-card style multi-section blank log grids (as per attached format).
  doc.addPage(pageOpts);
  drawRouteCardLogFormatPage();

  doc.end();
});

// POST /api/v1/production/route-card/generate/:workOrderId
exports.generateFromWorkOrder = asyncHandler(async (req, res) => {
  const { workOrderId } = req.params;
  const userId = req.user?.userId || null;

  try {
    const routeCard = await routeCardService.generateFromWorkOrder({ workOrderId, userId });
    const created = await db.RouteCard.findByPk(routeCard.id, {
      include: [
        { model: db.WorkOrder, as: 'workOrder', attributes: ['id', 'work_order_number', 'status'] },
        { model: db.Material, as: 'finishedMaterial', attributes: ['id', 'material_number', 'description', 'drawing_number', 'revision_number'] },
        { model: db.BOMHeader, as: 'bom', attributes: ['id', 'bom_number', 'drawing_number', 'revision_number'] },
        { model: db.RouteCardOperation, as: 'operations' }
      ],
      order: [[{ model: db.RouteCardOperation, as: 'operations' }, 'operation_sequence_no', 'ASC']]
    });
    sendResponse(res, 201, true, 'Route Card generated successfully', created);
  } catch (e) {
    sendError(res, e.message || 'Failed to generate Route Card', 400);
  }
});

// POST /api/v1/production/route-card/operation/:id/start
exports.startOperation = asyncHandler(async (req, res) => {
  const { id } = req.params;
  const userId = req.user?.userId || null;
  try {
    const op = await routeCardService.startOperation({ routeCardOperationId: id, userId });
    sendResponse(res, 200, true, 'Operation started successfully', op);
  } catch (e) {
    sendError(res, e.message || 'Failed to start operation', 400);
  }
});

// POST /api/v1/production/route-card/operation/:id/complete
exports.completeOperation = asyncHandler(async (req, res) => {
  const { id } = req.params;
  const userId = req.user?.userId || null;
  const data = req.body || {};
  try {
    const op = await routeCardService.completeOperation({ routeCardOperationId: id, data, userId });
    sendResponse(res, 200, true, 'Operation completed successfully', op);
  } catch (e) {
    sendError(res, e.message || 'Failed to complete operation', 400);
  }
});

// POST /api/v1/production/route-card/operation/:id/actuals
exports.updateActuals = asyncHandler(async (req, res) => {
  const { id } = req.params;
  const userId = req.user?.userId || null;
  const data = req.body || {};
  try {
    const op = await routeCardService.updateActuals({ routeCardOperationId: id, data, userId });
    sendResponse(res, 200, true, 'Actual times updated successfully', op);
  } catch (e) {
    sendError(res, e.message || 'Failed to update actual times', 400);
  }
});

// GET /api/v1/production/route-card/operation/:id/wip-consumption
exports.getWipOperationConsumption = asyncHandler(async (req, res) => {
  const opId = parseInt(req.params.id, 10);
  const userCompanyId = req.user?.companyId ?? req.user?.company_id;

  const op = await db.RouteCardOperation.findByPk(opId, {
    include: [{ model: db.RouteCard, as: 'routeCard', attributes: ['id', 'company_id'] }],
  });
  if (!op || !op.routeCard) return sendError(res, 'Route Card operation not found', 404);
  if (
    userCompanyId != null &&
    !req.user?.isSuperAdmin &&
    !req.user?.is_super_admin &&
    Number(op.routeCard.company_id) !== Number(userCompanyId)
  ) {
    return sendError(res, 'Forbidden', 403);
  }

  const rows = await productionWipService.getConsumptionRowsForOperation(opId);

  // Include already reversed qty (history) so UI can show: consumed gross, already reversed, available to reverse.
  const revRows = await db.RouteCardOperationMaterialReversal.findAll({
    where: { route_card_operation_id: opId },
    attributes: [
      'material_id',
      [db.sequelize.fn('SUM', db.sequelize.col('reversed_qty')), 'already_reversed_qty'],
    ],
    group: ['material_id'],
  });
  const revMap = new Map();
  for (const r of revRows) {
    const j = typeof r.toJSON === 'function' ? r.toJSON() : r;
    const mid = parseInt(j.material_id, 10);
    if (!(mid > 0)) continue;
    revMap.set(mid, round3(j.already_reversed_qty || 0));
  }

  const json = rows
    .map((row) => (typeof row.toJSON === 'function' ? row.toJSON() : row))
    .map((j) => {
      const mid = parseInt(j.material_id, 10);
      const alreadyReversed = mid > 0 ? (revMap.get(mid) || 0) : 0;
      const netConsumed = round3(j.consumed_qty ?? 0);
      const grossConsumed = round3(netConsumed + alreadyReversed);
      return {
        ...j,
        already_reversed_qty: alreadyReversed,
        consumed_gross_qty: grossConsumed,
        available_to_reverse_qty: netConsumed,
      };
    })
    .filter((j) => {
      const c = round3(j.consumed_qty ?? 0);
      const s = round3(j.scrap_qty ?? 0);
      return c + s > 0.0005;
    });

  sendResponse(res, 200, true, 'Saved consumption lines for this operation', json);
});

// PUT /api/v1/production/route-card/operation/:id/wip-consumption
exports.saveWipOperationConsumption = asyncHandler(async (req, res) => {
  const { id } = req.params;
  const opId = parseInt(id, 10);
  const userId = req.user?.userId || null;
  const userCompanyId = req.user?.companyId ?? req.user?.company_id;

  const op = await db.RouteCardOperation.findByPk(opId, {
    include: [{ model: db.RouteCard, as: 'routeCard', attributes: ['id', 'company_id', 'production_order_id'] }]
  });
  if (!op || !op.routeCard) return sendError(res, 'Route Card operation not found', 404);
  if (
    userCompanyId != null &&
    !req.user?.isSuperAdmin &&
    !req.user?.is_super_admin &&
    Number(op.routeCard.company_id) !== Number(userCompanyId)
  ) {
    return sendError(res, 'Forbidden', 403);
  }

  const lines = req.body?.lines;
  const transaction = await db.sequelize.transaction();
  try {
    await productionWipService.saveOperationConsumptions(transaction, {
      routeCardOperationId: opId,
      userId,
      lines
    });
    await transaction.commit();
    const consumptions = await productionWipService.getConsumptionRowsForOperation(opId);
    const poId = op.routeCard?.production_order_id;
    let production_wip_balances = [];
    if (poId > 0) {
      const balRows = await productionWipService.listBalancesForProductionOrder(poId);
      const { rawMaterialRows: rawForFilter, nestingRawMaterialLineByMaterialId, nestingRawMaterialLineByPartMaterialId } =
        await buildNestingRawMaterialDisplayMaps(poId);
      const allowedFastenerIdSet = await loadAllowedFastenerMaterialIdSetForProductionOrder(poId);
      const saveNestSheetSet = new Set();
      for (const r of rawForFilter) {
        if (r.nesting_stores_request_line_id == null || r.material_id == null) continue;
        const id = parseInt(r.material_id, 10);
        if (Number.isFinite(id) && id > 0) saveNestSheetSet.add(id);
      }
      const saveHasNestingLines = saveNestSheetSet.size > 0;
      const balRowsSaveApi =
        saveHasNestingLines
          ? balRows.filter((b) => {
              const j = typeof b.toJSON === 'function' ? b.toJSON() : b;
              const mat = j.material || null;
              if (isFastenerForWipDisplay(mat)) {
                const mid = parseInt(j.material_id, 10);
                return Number.isFinite(mid) && allowedFastenerIdSet.has(mid);
              }
              const mb = parseInt(j.material_id, 10);
              return Number.isFinite(mb) && saveNestSheetSet.has(mb);
            })
          : // No nesting→stores sheet lines: do not show raw material pool; allow only fasteners sent to stores via PO MR.
            balRows.filter((b) => {
              const j = typeof b.toJSON === 'function' ? b.toJSON() : b;
              const mat = j.material || null;
              if (!isFastenerForWipDisplay(mat)) return false;
              const mid = parseInt(j.material_id, 10);
              return Number.isFinite(mid) && allowedFastenerIdSet.has(mid);
            });
      production_wip_balances = balRowsSaveApi.map((b) => {
        const j = typeof b.toJSON === 'function' ? b.toJSON() : b;
        const mat = j.material || null;
        const midBal = parseInt(j.material_id, 10);
        const nestBal = resolveNestingDisplayForWipMaterial(
          midBal,
          mat,
          nestingRawMaterialLineByPartMaterialId,
          nestingRawMaterialLineByMaterialId
        );
        const displayBal = nestBal ? nestBal : buildWipMaterialDisplayString(mat);
        return {
          material_id: j.material_id,
          material_number: mat?.material_number || '',
          description: mat?.description || '',
          material_type: mat?.material_type || '',
          material_category: wipMaterialCategoryLabel(mat),
          material_display: displayBal,
          wip_row_kind: isFastenerForWipDisplay(mat) ? 'FASTENER' : 'RAW_MATERIAL',
          available_qty: round3(j.available_qty),
          uom_id: j.uom_id || j.uom?.id || null,
          uom_code: j.uom?.uom_code || ''
        };
      });
    }
    sendResponse(res, 200, true, 'Production Storage consumption saved', {
      consumptions,
      production_wip_balances
    });
  } catch (e) {
    try {
      await transaction.rollback();
    } catch (_) {
      /* already finished */
    }
    sendError(res, e.message || 'Failed to save consumption', 400);
  }
});

// POST /api/v1/production/route-card/operation/:id/wip-reverse
exports.reverseWipOperationConsumption = asyncHandler(async (req, res) => {
  const opId = parseInt(req.params.id, 10);
  const userId = req.user?.userId || null;
  const userCompanyId = req.user?.companyId ?? req.user?.company_id;

  const op = await db.RouteCardOperation.findByPk(opId, {
    include: [{ model: db.RouteCard, as: 'routeCard', attributes: ['id', 'company_id', 'production_order_id'] }],
  });
  if (!op || !op.routeCard) return sendError(res, 'Route Card operation not found', 404);
  if (
    userCompanyId != null &&
    !req.user?.isSuperAdmin &&
    !req.user?.is_super_admin &&
    Number(op.routeCard.company_id) !== Number(userCompanyId)
  ) {
    return sendError(res, 'Forbidden', 403);
  }

  const materialId = req.body?.material_id ?? req.body?.materialId;
  const reverseQty = req.body?.reverse_qty ?? req.body?.reverseQty ?? req.body?.qty;
  const remarks = req.body?.remarks;

  const transaction = await db.sequelize.transaction();
  try {
    const out = await productionWipService.reverseOperationConsumption(transaction, {
      routeCardOperationId: opId,
      materialId,
      reverseQty,
      userId,
      remarks,
    });
    await transaction.commit();

    // Refresh payload like save API so UI can update immediately.
    const consumptions = await productionWipService.getConsumptionRowsForOperation(opId);
    const poId = op.routeCard?.production_order_id;
    let production_wip_balances = [];
    if (poId > 0) {
      const balRows = await productionWipService.listBalancesForProductionOrder(poId);
      production_wip_balances = (balRows || []).map((b) => (typeof b.toJSON === 'function' ? b.toJSON() : b));
    }

    sendResponse(res, 200, true, 'Consumption reversed successfully', {
      reversal: out,
      consumptions,
      production_wip_balances,
    });
  } catch (e) {
    try {
      await transaction.rollback();
    } catch (_) {
      /* ignore */
    }
    sendError(res, e.message || 'Failed to reverse consumption', 400);
  }
});

// POST /api/v1/production/route-card/operation/:id/cancel
exports.cancelOperation = asyncHandler(async (req, res) => {
  const { id } = req.params;
  const opId = parseInt(id, 10);
  const userId = req.user?.userId || null;
  const userCompanyId = req.user?.companyId ?? req.user?.company_id;

  const op = await db.RouteCardOperation.findByPk(opId, {
    include: [{ model: db.RouteCard, as: 'routeCard', attributes: ['id', 'company_id'] }]
  });
  if (!op || !op.routeCard) return sendError(res, 'Route Card operation not found', 404);
  if (
    userCompanyId != null &&
    !req.user?.isSuperAdmin &&
    !req.user?.is_super_admin &&
    Number(op.routeCard.company_id) !== Number(userCompanyId)
  ) {
    return sendError(res, 'Forbidden', 403);
  }

  const remarks = req.body?.remarks;
  try {
    const updated = await routeCardService.cancelOperation({ routeCardOperationId: opId, userId, remarks });
    sendResponse(res, 200, true, 'Operation cancelled; material returned to Production Storage (WIP)', updated);
  } catch (e) {
    sendError(res, e.message || 'Failed to cancel operation', 400);
  }
});
