(function (root, factory) {
  "use strict";
  const api = factory();
  if (typeof module === "object" && module.exports) module.exports = api;
  if (root) root.JIANGLAI_QUOTE_AUTO_ROUTER = Object.freeze(api);
})(typeof globalThis !== "undefined" ? globalThis : this, function () {
  "use strict";

  const INDUSTRY_FAMILIES = new Set(["area", "yield", "weight", "unit", "bom", "multipart", "manual"]);
  const ROLL_WIDTHS_MM = Object.freeze([160, 210, 250, 330, 420, 520]);
  const ROLL_FIELD_IDS = Object.freeze([
    "quoteRollWebWidth", "quoteRollSideMargin", "quoteRollGap", "quoteRollSetupWaste",
    "quoteRollWasteRate", "quoteRollMaterialPrice", "quoteRollMaterialUnit", "quoteRollGsm",
    "quoteRollPrintSetupFee", "quoteRollPrintUnitRate", "quoteRollSpeed"
  ]);

  function resolve(input = {}) {
    const profile = String(input.profile || "");
    const family = String(input.family || "manual");
    const hasPrintTier = Boolean(input.hasPrintTier);
    if (profile === "book") return Object.freeze({ mode: "printing", calculator: "book", family, automatic: true });
    if (family === "roll") return Object.freeze({ mode: "printing", calculator: "roll", family, automatic: true });
    if (["sheet", "dieline"].includes(family)) {
      return Object.freeze({ mode: hasPrintTier ? "printing" : "material", calculator: hasPrintTier ? "sheet" : "material", family, automatic: true });
    }
    if (family === "book") return Object.freeze({ mode: "industry", calculator: "industry", family, automatic: true });
    if (INDUSTRY_FAMILIES.has(family)) return Object.freeze({ mode: "industry", calculator: "industry", family, automatic: true });
    return Object.freeze({ mode: "industry", calculator: "industry", family: family || "manual", automatic: true });
  }

  function number(value, fallback = 0) {
    const parsed = Number(String(value ?? "").trim().replace(/,/g, ""));
    return Number.isFinite(parsed) ? parsed : fallback;
  }

  function positive(value, label) {
    const parsed = number(value);
    if (!(parsed > 0)) throw new Error(`请填写${label}`);
    return parsed;
  }

  function nonNegative(value, label) {
    const parsed = number(value);
    if (!(parsed >= 0)) throw new Error(`${label}不能小于0`);
    return parsed;
  }

  function rate(value, label) {
    const parsed = nonNegative(value, label);
    const normalized = parsed > 1 ? parsed / 100 : parsed;
    if (normalized >= 1) throw new Error(`${label}必须小于100%`);
    return normalized;
  }

  function parseSize(value) {
    const parts = String(value || "").trim().split(/[xX×*]/).map(item => number(item.replace(/[^\d.]/g, ""))).filter(item => item > 0);
    if (parts.length < 2) throw new Error("标签尺寸需要宽和高，例如 50×30mm");
    return Object.freeze({ widthMm: parts[0], heightMm: parts[1] });
  }

  function selectedText(configuration, groupId) {
    const group = (configuration?.groups || []).find(item => item.id === groupId);
    return String((group?.values || []).join(" ")).trim();
  }

  function normalizeMaterialUnit(value) {
    const unit = String(value || "").replace(/\s+/g, "").toLowerCase();
    if (unit.includes("吨") || unit.includes("ton")) return "元/吨";
    if (unit.includes("kg") || unit.includes("公斤") || unit.includes("千克")) return "元/kg";
    if (unit.includes("㎡") || unit.includes("m²") || unit.includes("m2") || unit.includes("平方")) return "元/㎡";
    if (unit.includes("米") || /\/m$/.test(unit)) return "元/米";
    if (unit.includes("张")) return "元/张";
    return "元/个";
  }

  function rollLayout(input = {}) {
    const labelWidthMm = positive(input.labelWidthMm, "标签宽");
    const labelHeightMm = positive(input.labelHeightMm, "标签高");
    const webWidthMm = positive(input.webWidthMm, "卷材宽度");
    const sideMarginMm = nonNegative(input.sideMarginMm, "卷材安全边");
    const gapMm = nonNegative(input.gapMm, "标签间隙");
    const usableWidthMm = webWidthMm - sideMarginMm * 2;
    if (!(usableWidthMm > 0)) throw new Error("卷材宽度必须大于两侧安全边");
    const candidates = [
      { rotated: false, crossMm: labelWidthMm, pitchMm: labelHeightMm },
      { rotated: true, crossMm: labelHeightMm, pitchMm: labelWidthMm }
    ].map(candidate => {
      const lanes = Math.floor((usableWidthMm + gapMm) / (candidate.crossMm + gapMm));
      return { ...candidate, lanes, unitAreaMm2: lanes > 0 ? webWidthMm * (candidate.pitchMm + gapMm) / lanes : Infinity };
    }).filter(candidate => candidate.lanes > 0).sort((left, right) => left.unitAreaMm2 - right.unitAreaMm2 || right.lanes - left.lanes);
    if (!candidates.length) throw new Error("当前卷材宽度放不下这个标签，请增大卷材宽度");
    return Object.freeze({ ...candidates[0], usableWidthMm, webWidthMm, sideMarginMm, gapMm });
  }

  function calculateRoll(input = {}) {
    const quantity = Math.ceil(positive(input.quantity, "产品数量"));
    const setupWasteMeters = nonNegative(input.setupWasteMeters, "起机走料");
    const wasteRate = rate(input.wasteRate, "生产损耗率");
    const layout = rollLayout(input);
    const rows = Math.ceil(quantity / layout.lanes);
    const netLengthMeters = rows * (layout.pitchMm + layout.gapMm) / 1000;
    const productionLengthMeters = (netLengthMeters + setupWasteMeters) / (1 - wasteRate);
    const netAreaM2 = layout.webWidthMm / 1000 * netLengthMeters;
    const productionAreaM2 = layout.webWidthMm / 1000 * productionLengthMeters;
    const materialPrice = positive(input.materialPrice, "材料采购价");
    const materialUnit = normalizeMaterialUnit(input.materialUnit);
    const gsm = number(input.gsm);
    let materialUsage;
    let materialUsageUnit;
    if (materialUnit === "元/㎡") {
      materialUsage = productionAreaM2;
      materialUsageUnit = "㎡";
    } else if (materialUnit === "元/米") {
      materialUsage = productionLengthMeters;
      materialUsageUnit = "米";
    } else if (materialUnit === "元/kg" || materialUnit === "元/吨") {
      if (!(gsm > 0)) throw new Error("当前材料按重量采购，请填写材料克重");
      materialUsage = productionAreaM2 * gsm / 1000;
      materialUsageUnit = "kg";
    } else {
      materialUsage = quantity;
      materialUsageUnit = "个";
    }
    const materialPricePerUnit = materialUnit === "元/吨" ? materialPrice / 1000 : materialPrice;
    const materialCost = materialUsage * materialPricePerUnit;
    const printingEnabled = input.printingEnabled !== false;
    const printSetupFee = printingEnabled ? nonNegative(input.printSetupFee, "印刷开机费") : 0;
    const printUnitRate = printingEnabled ? nonNegative(input.printUnitRate, "标签印刷单价") : 0;
    const printingCost = printSetupFee + quantity * printUnitRate;
    const speedMetersPerHour = number(input.speedMetersPerHour);
    const productionHours = speedMetersPerHour > 0 ? productionLengthMeters / speedMetersPerHour : 0;
    const materialWeightKg = gsm > 0 ? productionAreaM2 * gsm / 1000 : 0;
    const operationContext = {
      quantity,
      rows,
      productionLengthMeters,
      productionAreaM2,
      materialWeightKg,
      productionHours
    };
    const operationItems = (Array.isArray(input.operations) ? input.operations : []).map(operation => {
      const basis = String(operation.basis || "piece");
      const basisQuantity = basis === "area_m2" ? productionAreaM2
        : basis === "meter" ? productionLengthMeters
          : basis === "kg" ? materialWeightKg
            : basis === "run" ? Math.max(1, number(operation.usagePerPiece, 1))
              : basis === "hour" ? productionHours
                : quantity;
      const unitRate = nonNegative(operation.unitRate ?? operation.unitPrice, `${operation.name || "工艺"}单价`);
      const setupFee = nonNegative(operation.setupFee, `${operation.name || "工艺"}固定费`);
      const minimumCharge = nonNegative(operation.minimumCharge, `${operation.name || "工艺"}最低收费`);
      const chargeMode = String(operation.chargeMode || "minimum");
      const packageQuantity = nonNegative(operation.packageQuantity ?? operation.includedQuantity, `${operation.name || "工艺"}套餐数量`);
      const packageFee = nonNegative(operation.packageFee ?? operation.minimumCharge, `${operation.name || "工艺"}套餐费`);
      const variable = chargeMode === "package_plus_excess"
        ? packageFee + Math.max(0, basisQuantity - packageQuantity) * unitRate
        : basisQuantity * unitRate;
      const total = setupFee + (chargeMode === "minimum" ? Math.max(variable, minimumCharge) : variable);
      return Object.freeze({
        id: String(operation.id || operation.name || "roll-operation"),
        name: String(operation.name || "卷材后道"),
        basis,
        basisQuantity,
        unitRate,
        minimumCharge,
        total
      });
    });
    const operationsCost = operationItems.reduce((sum, item) => sum + item.total, 0);
    const productionRows = Math.ceil(productionLengthMeters * 1000 / (layout.pitchMm + layout.gapMm));
    return Object.freeze({
      family: "roll",
      quantity,
      layout,
      roll: Object.freeze({ rows, productionRows, netLengthMeters, productionLengthMeters, netAreaM2, productionAreaM2 }),
      sheets: Object.freeze({ netSheets: rows, productionSheets: productionRows }),
      paper: Object.freeze({ weightKg: materialWeightKg }),
      impressions: quantity,
      plateCount: printingEnabled ? 1 : 0,
      productionHours,
      material: Object.freeze({ unit: materialUnit, purchasePrice: materialPrice, pricePerUsageUnit: materialPricePerUnit, usage: materialUsage, usageUnit: materialUsageUnit }),
      costs: Object.freeze({ material: materialCost, printing: printingCost, labor: 0, machine: 0, operations: operationsCost, overhead: 0, total: materialCost + printingCost + operationsCost }),
      operationItems: Object.freeze(operationItems),
      operationContext: Object.freeze(operationContext)
    });
  }

  function recommendRollInputs(input = {}) {
    const size = parseSize(input.size);
    const quantity = Math.max(1, Math.ceil(number(input.quantity, 1000)));
    const sideMarginMm = number(input.sideMarginMm, 3);
    const gapMm = number(input.gapMm, 3);
    const setupWasteMeters = number(input.setupWasteMeters, 20);
    const wasteRate = number(input.wasteRate, 5);
    const allowedWidths = ROLL_WIDTHS_MM.filter(width => width >= Math.min(size.widthMm, size.heightMm) + sideMarginMm * 2);
    const candidates = allowedWidths.map(webWidthMm => {
      try {
        const layout = rollLayout({ labelWidthMm: size.widthMm, labelHeightMm: size.heightMm, webWidthMm, sideMarginMm, gapMm });
        const rows = Math.ceil(quantity / layout.lanes);
        const netLengthMeters = rows * (layout.pitchMm + gapMm) / 1000;
        const productionLengthMeters = (netLengthMeters + setupWasteMeters) / (1 - wasteRate / 100);
        return { webWidthMm, layout, productionLengthMeters, productionAreaM2:webWidthMm / 1000 * productionLengthMeters };
      } catch (_) {
        return null;
      }
    }).filter(Boolean).sort((left, right) => left.productionAreaM2 - right.productionAreaM2 || left.webWidthMm - right.webWidthMm);
    const recommended = candidates[0] || { webWidthMm: Math.ceil(Math.min(size.widthMm, size.heightMm) + sideMarginMm * 2), layout:null };
    return Object.freeze({ ...size, quantity, sideMarginMm, gapMm, setupWasteMeters, wasteRate, webWidthMm:recommended.webWidthMm, layout:recommended.layout });
  }

  function escapeHtml(value) {
    return String(value ?? "").replace(/[&<>"']/g, char => ({ "&":"&amp;", "<":"&lt;", ">":"&gt;", '"':"&quot;", "'":"&#39;" }[char]));
  }

  function rollDefaults(context = {}) {
    const previous = context.previous || {};
    const recommendation = recommendRollInputs({
      size: context.size,
      quantity: context.quantity,
      webWidthMm: previous.quoteRollWebWidth,
      sideMarginMm: previous.quoteRollSideMargin || 3,
      gapMm: previous.quoteRollGap || 3,
      setupWasteMeters: previous.quoteRollSetupWaste || 20,
      wasteRate: previous.quoteRollWasteRate || 5
    });
    const materialSource = context.materialSource || {};
    const printingEnabled = !/不印刷|无需印刷/.test(selectedText(context.configuration, "print_color"));
    return Object.freeze({
      recommendation,
      printingEnabled,
      webWidthMm: previous.quoteRollWebWidth || recommendation.webWidthMm,
      sideMarginMm: previous.quoteRollSideMargin || recommendation.sideMarginMm,
      gapMm: previous.quoteRollGap || recommendation.gapMm,
      setupWasteMeters: previous.quoteRollSetupWaste || recommendation.setupWasteMeters,
      wasteRate: previous.quoteRollWasteRate || recommendation.wasteRate,
      materialPrice: previous.quoteRollMaterialPrice || materialSource.price || "",
      materialUnit: previous.quoteRollMaterialUnit || normalizeMaterialUnit(materialSource.unit),
      gsm: previous.quoteRollGsm || String(`${context.materialName || ""} ${materialSource.name || ""} ${materialSource.spec || ""}`).match(/(\d+(?:\.\d+)?)\s*(?:g|克)/i)?.[1] || "",
      printSetupFee: previous.quoteRollPrintSetupFee || (printingEnabled ? number(context.settings?.print, 0) : 0),
      printUnitRate: previous.quoteRollPrintUnitRate || (printingEnabled ? number(context.settings?.printUnit, 0) : 0),
      speedMetersPerHour: previous.quoteRollSpeed || 0
    });
  }

  function renderRoll(context = {}) {
    let defaults;
    try {
      defaults = rollDefaults(context);
    } catch (error) {
      return `<div class="member-note"><strong>${escapeHtml(context.entryName || "卷材产品")} · 自动卷材公式</strong><br>${escapeHtml(error.message)}</div>`;
    }
    const delivery = selectedText(context.configuration, "delivery_form") || "按当前交付形式";
    const core = selectedText(context.configuration, "core_direction") || "无需卷芯";
    let preview = "";
    try {
      const result = calculateRoll({
        quantity: context.quantity,
        labelWidthMm: defaults.recommendation.widthMm,
        labelHeightMm: defaults.recommendation.heightMm,
        webWidthMm: defaults.webWidthMm,
        sideMarginMm: defaults.sideMarginMm,
        gapMm: defaults.gapMm,
        setupWasteMeters: defaults.setupWasteMeters,
        wasteRate: defaults.wasteRate,
        materialPrice: defaults.materialPrice,
        materialUnit: defaults.materialUnit,
        gsm: defaults.gsm,
        printingEnabled: defaults.printingEnabled,
        printSetupFee: defaults.printSetupFee,
        printUnitRate: defaults.printUnitRate,
        speedMetersPerHour: defaults.speedMetersPerHour
      });
      preview = `已自动选择 ${result.layout.webWidthMm}mm 卷材，${result.layout.lanes} 排${result.layout.rotated ? "（旋转走料）" : ""}；预计走料 ${result.roll.productionLengthMeters.toFixed(2)}m、用料 ${result.roll.productionAreaM2.toFixed(3)}㎡，材料成本约 ¥${result.costs.material.toFixed(2)}。`;
    } catch (error) {
      preview = `已切换到卷材公式；${error.message}。`;
    }
    const option = unit => `<option value="${unit}" ${defaults.materialUnit === unit ? "selected" : ""}>${unit}</option>`;
    return `
      <div class="member-note" style="margin:8px 0 10px"><strong>${escapeHtml(context.entryName || "卷材产品")} · 卷材排版/走料公式已自动启用</strong><br>${escapeHtml(preview)}<br><span class="muted">材料：${escapeHtml(context.materialSource?.name || context.materialName || "待匹配")} ${escapeHtml(context.materialSource?.spec || "")}；交付：${escapeHtml(delivery)}；卷筒：${escapeHtml(core)}。同属卷材结构的标签会自动继承这套公式，不再按产品逐个配置。</span></div>
      <details class="quote-cost-advanced"><summary>查看或修正卷材生产参数（系统已预填）</summary>
        <div class="mini-grid">
          <div><label class="field-label">卷材宽度（mm）</label><input id="quoteRollWebWidth" inputmode="decimal" value="${escapeHtml(defaults.webWidthMm)}"></div>
          <div><label class="field-label">两侧安全边（mm）</label><input id="quoteRollSideMargin" inputmode="decimal" value="${escapeHtml(defaults.sideMarginMm)}"></div>
          <div><label class="field-label">标签间隙（mm）</label><input id="quoteRollGap" inputmode="decimal" value="${escapeHtml(defaults.gapMm)}"></div>
          <div><label class="field-label">起机走料（米）</label><input id="quoteRollSetupWaste" inputmode="decimal" value="${escapeHtml(defaults.setupWasteMeters)}"></div>
          <div><label class="field-label">生产损耗率（%）</label><input id="quoteRollWasteRate" inputmode="decimal" value="${escapeHtml(defaults.wasteRate)}"></div>
          <div><label class="field-label">材料采购价</label><input id="quoteRollMaterialPrice" inputmode="decimal" value="${escapeHtml(defaults.materialPrice)}" placeholder="自动匹配本厂成本库"></div>
          <div><label class="field-label">材料计价单位</label><select id="quoteRollMaterialUnit">${["元/㎡","元/米","元/kg","元/吨","元/张","元/个"].map(option).join("")}</select></div>
          <div><label class="field-label">材料克重（按kg/吨采购时）</label><input id="quoteRollGsm" inputmode="decimal" value="${escapeHtml(defaults.gsm)}" placeholder="按面积采购可留空"></div>
          <div><label class="field-label">印刷开机费（元/单）</label><input id="quoteRollPrintSetupFee" inputmode="decimal" value="${escapeHtml(defaults.printSetupFee)}"></div>
          <div><label class="field-label">卷材基础印刷（元/个）</label><input id="quoteRollPrintUnitRate" inputmode="decimal" value="${escapeHtml(defaults.printUnitRate)}"></div>
          <div><label class="field-label">设备速度（米/小时，可留空）</label><input id="quoteRollSpeed" inputmode="decimal" value="${escapeHtml(defaults.speedMetersPerHour)}"></div>
        </div>
        <div class="field-help">卷宽、间隙、起机走料和损耗是可修正的生产参数；材料价格优先读取当前启用的本厂成本库。公开参考数据只用于首次预填，不会覆盖已保存的本厂值。</div>
      </details>
    `;
  }

  function breakdownRoll(context = {}) {
    try {
      const defaults = rollDefaults(context);
      const values = context.values || {};
      const read = (id, fallback) => String(values[id] ?? "").trim() || fallback;
      const result = calculateRoll({
        quantity: context.quantity,
        labelWidthMm: defaults.recommendation.widthMm,
        labelHeightMm: defaults.recommendation.heightMm,
        webWidthMm: read("quoteRollWebWidth", defaults.webWidthMm),
        sideMarginMm: read("quoteRollSideMargin", defaults.sideMarginMm),
        gapMm: read("quoteRollGap", defaults.gapMm),
        setupWasteMeters: read("quoteRollSetupWaste", defaults.setupWasteMeters),
        wasteRate: read("quoteRollWasteRate", defaults.wasteRate),
        materialPrice: read("quoteRollMaterialPrice", defaults.materialPrice),
        materialUnit: read("quoteRollMaterialUnit", defaults.materialUnit),
        gsm: read("quoteRollGsm", defaults.gsm),
        printingEnabled: defaults.printingEnabled,
        printSetupFee: read("quoteRollPrintSetupFee", defaults.printSetupFee),
        printUnitRate: read("quoteRollPrintUnitRate", defaults.printUnitRate),
        speedMetersPerHour: read("quoteRollSpeed", defaults.speedMetersPerHour),
        operations: context.operations
      });
      const source = context.materialSource || {};
      const library = context.costLibrary || {};
      const operationNames = (context.operations || []).map(item => item.name).filter(Boolean);
      return {
        ok: true,
        category: "卷材标签",
        materialCost: result.costs.material,
        materialUnitCost: result.costs.material / Math.max(result.quantity, 1),
        printingCost: result.costs.printing,
        operationCost: result.costs.operations,
        operationItems: result.operationItems,
        extraDirectCost: 0,
        laborCost: 0,
        machineCost: 0,
        materialLabel: `${source.name || context.materialName || "卷材"} ${result.material.purchasePrice.toFixed(3)}${result.material.unit}；${result.layout.lanes}排，走料 ${result.roll.productionLengthMeters.toFixed(3)}m，用料 ${result.roll.productionAreaM2.toFixed(3)}㎡`,
        extraLabel: "",
        basis: `卷材自动排版：${result.layout.webWidthMm}mm卷宽，${result.layout.lanes}排${result.layout.rotated ? "旋转" : "正向"}走料；净走料 ${result.roll.netLengthMeters.toFixed(3)}m，含起机与损耗 ${result.roll.productionLengthMeters.toFixed(3)}m。`,
        lossLabel: `净走料 ${result.roll.netLengthMeters.toFixed(3)}m → 含起机/损耗走料 ${result.roll.productionLengthMeters.toFixed(3)}m，已计入材料成本`,
        formulaInput: {
          family: "roll",
          size: context.size,
          quantity: context.quantity,
          configuration: context.configuration,
          values: Object.fromEntries(ROLL_FIELD_IDS.map(id => [id, read(id, defaults[id])]))
        },
        formulaResult: result,
        libraryOperationIds: (context.operations || []).map(item => item.id).filter(Boolean),
        libraryOperationNames: operationNames,
        usedCostItems: (context.usedCostItems || []).filter(Boolean),
        hourlyRates: { labor:{ rate:0, source:"included" }, machine:{ rate:0, source:"included" }, allInPrintPrice:true },
        costVersionId: String(library.activeVersionId || ""),
        costVersionEffectiveAt: String(library.effectiveAt || "")
      };
    } catch (error) {
      const message = String(error?.message || error || "卷材成本计算失败");
      const focusMap = [
        ["卷材宽度", "quoteRollWebWidth"], ["安全边", "quoteRollSideMargin"], ["间隙", "quoteRollGap"],
        ["起机走料", "quoteRollSetupWaste"], ["损耗", "quoteRollWasteRate"], ["采购价", "quoteRollMaterialPrice"],
        ["克重", "quoteRollGsm"], ["印刷开机费", "quoteRollPrintSetupFee"], ["印刷单价", "quoteRollPrintUnitRate"]
      ];
      return { ok:false, error:`卷材自动公式还缺数据：${message}`, focusFieldId:focusMap.find(item => message.includes(item[0]))?.[1] || "" };
    }
  }

  return Object.freeze({
    schemaVersion: 1,
    rollFieldIds: ROLL_FIELD_IDS,
    resolve,
    parseSize,
    normalizeMaterialUnit,
    rollLayout,
    calculateRoll,
    recommendRollInputs,
    renderRoll,
    breakdownRoll
  });
});
