const client = require("../graphql-client/graphql-client").client;
const { GET_JOB_BY_PK } = require("../graphql-client/queries");

/**
 * Remove all non-digit characters from a string.
 * @param s
 * @returns {string}
 */
const digitsOnly = (s) => String(s || "").replace(/\D/g, "");

/**
 * Pick job ID from various possible locations.
 * @param ctx
 * @param explicitId
 * @returns {*|null}
 */
const pickJobId = (ctx, explicitId) =>
  explicitId || ctx?.job?.id || ctx?.payload?.job?.id || ctx?.payload?.jobId || ctx?.jobId || null;

/**
 * Safely get VIN from job object.
 * @param job
 * @returns {*|string|null}
 */
const safeVin = (job) => (job?.v_vin && String(job.v_vin).trim()) || null;

/**
 * Extract blocks array from combined search result.
 * @param res
 * @returns {any[]|*[]}
 */
const blocksFromCombinedSearchResult = (res) => {
  const data = Array.isArray(res?.data) ? res.data : Array.isArray(res) ? res : [];
  return Array.isArray(data) ? data : [];
};

/**
 * Convert a Dinero.js object or number into an "N2" string ("123.45").
 * @param value
 * @returns {string}
 */
const asN2 = (dineroLike) => {
  if (!dineroLike) return "0.00";

  // Handle Dinero v1/v2-ish or raw objects
  if (typeof dineroLike.toUnit === "function") {
    return dineroLike.toUnit().toFixed(2);
  }

  const precision = dineroLike.precision ?? 2;
  const amount = (dineroLike.amount ?? 0) / Math.pow(10, precision);
  return amount.toFixed(2);
};

const toFiniteNumber = (value) => {
  if (typeof value === "number") {
    return Number.isFinite(value) ? value : 0;
  }

  if (typeof value === "string") {
    const parsed = Number.parseFloat(value);
    return Number.isFinite(parsed) ? parsed : 0;
  }

  return 0;
};

/**
 * Normalize various "money-like" shapes to integer cents.
 * Supports:
 *  - Dinero instances (getAmount / toUnit)
 *  - { cents }
 *  - { amount, precision }
 *  - plain numbers (treated as units, e.g. dollars)
 *  - numeric strings (treated as units, e.g. "123.45")
 * @param value
 * @returns {number}
 */
const toMoneyCents = (value) => {
  if (value == null || value === "") return 0;

  if (typeof value.getAmount === "function") {
    return value.getAmount();
  }

  if (typeof value.toUnit === "function") {
    const unit = value.toUnit();
    return Number.isFinite(unit) ? Math.round(unit * 100) : 0;
  }

  if (typeof value.cents === "number") {
    return value.cents;
  }

  if (typeof value.amount === "number") {
    const precision = typeof value.precision === "number" ? value.precision : 2;
    if (precision === 2) return value.amount;
    const factor = Math.pow(10, 2 - precision);
    return Math.round(value.amount * factor);
  }

  if (typeof value === "number") {
    return Math.round(value * 100);
  }

  if (typeof value === "string") {
    const parsed = Number.parseFloat(value);
    return Number.isFinite(parsed) ? Math.round(parsed * 100) : 0;
  }

  return 0;
};

const asN2FromCents = (cents) => asN2({ amount: Number.isFinite(cents) ? cents : 0, precision: 2 });

const formatDecimal = (value, maxDecimals = 2) => {
  const factor = Math.pow(10, maxDecimals);
  const rounded = Math.round(Math.max(0, toFiniteNumber(value)) * factor) / factor;
  if (!Number.isFinite(rounded)) return "0";
  return rounded.toFixed(maxDecimals).replace(/\.?0+$/, "") || "0";
};

const isLaborSideProfitCenter = (alloc = {}) => {
  const pc = alloc?.profitCenter || {};

  if (
    pc.rr_requires_rolabor ||
    pc.rr_force_rolabor ||
    pc.rr_labor_side ||
    pc.rr_is_labor ||
    pc.is_labor
  ) {
    return true;
  }

  return [alloc.center, pc.name, pc.accountdesc, pc.accountname].some((value) => /\blabou?r\b/i.test(String(value || "")));
};

const buildRolaborBillFields = ({ amountUnits = 0, hours = 0, rate = 0 } = {}) => {
  const normalizedAmount = toFiniteNumber(amountUnits);

  if (normalizedAmount <= 0) {
    return {
      jobTotalHrs: "0",
      billTime: "0",
      billRate: "0"
    };
  }

  let resolvedHours = toFiniteNumber(hours);
  let resolvedRate = toFiniteNumber(rate);

  if (resolvedHours > 0 && resolvedRate <= 0) {
    resolvedRate = normalizedAmount / resolvedHours;
  } else if (resolvedRate > 0 && resolvedHours <= 0) {
    resolvedHours = normalizedAmount / resolvedRate;
  } else if (resolvedHours <= 0 && resolvedRate <= 0) {
    // Keep the math internally consistent even if the source job has dollars but no usable hours.
    resolvedHours = 1;
    resolvedRate = normalizedAmount;
  }

  return {
    jobTotalHrs: formatDecimal(resolvedHours),
    billTime: formatDecimal(resolvedHours),
    billRate: resolvedRate.toFixed(2)
  };
};

const buildMinimalRolaborFromJob = (job, { opCode, payType = "Cust" } = {}) => {
  const trimmedOpCode = opCode != null ? String(opCode).trim() : "";
  if (!job || !trimmedOpCode) return null;

  let totalHours = 0;
  let totalAmountUnits = 0;

  for (const line of job?.joblines || []) {
    const laborType = typeof line?.mod_lbr_ty === "string" ? line.mod_lbr_ty.trim() : "";
    if (!laborType) continue;

    const lineHours = toFiniteNumber(line?.mod_lb_hrs ?? line?.db_hrs);
    const configuredRate = toFiniteNumber(job?.[`rate_${laborType.toLowerCase()}`]);
    let lineAmountUnits = toFiniteNumber(line?.lbr_amt);

    if (lineAmountUnits <= 0 && lineHours > 0 && configuredRate > 0) {
      lineAmountUnits = lineHours * configuredRate;
    }

    if (lineAmountUnits <= 0 && lineHours <= 0) continue;

    totalHours += lineHours;
    totalAmountUnits += lineAmountUnits;
  }

  if (totalAmountUnits <= 0 && totalHours <= 0) return null;

  const bill = buildRolaborBillFields({
    amountUnits: totalAmountUnits,
    hours: totalHours,
    rate: totalHours > 0 ? totalAmountUnits / totalHours : 0
  });
  const formattedAmount = totalAmountUnits.toFixed(2);

  return {
    ops: [
      {
        opCode: trimmedOpCode,
        jobNo: "1",
        custPayTypeFlag: "C",
        custTxblNtxblFlag: toFiniteNumber(job?.tax_lbr_rt) > 0 ? "T" : "N",
        bill: {
          payType,
          ...bill
        },
        amount: {
          payType,
          amtType: "Job",
          custPrice: formattedAmount,
          totalAmt: formattedAmount
        }
      }
    ]
  };
};

/**
 * Build RR estimate block from allocation totals.
 * @param {Array} allocations
 * @returns {{parts: string, labor: string, total: string}|null}
 */
const buildEstimateFromAllocations = (allocations) => {
  if (!Array.isArray(allocations) || allocations.length === 0) return null;

  const totals = allocations.reduce(
    (acc, alloc) => {
      acc.parts += toMoneyCents(alloc?.partsSale);
      acc.labor += toMoneyCents(alloc?.laborTaxableSale);
      acc.labor += toMoneyCents(alloc?.laborNonTaxableSale);
      acc.total += toMoneyCents(alloc?.totalSale);
      return acc;
    },
    { parts: 0, labor: 0, total: 0 }
  );

  // If totalSale wasn't provided, keep total coherent with parts + labor.
  if (!totals.total) {
    totals.total = totals.parts + totals.labor;
  }

  return {
    parts: asN2FromCents(totals.parts),
    labor: asN2FromCents(totals.labor),
    total: asN2FromCents(totals.total)
  };
};

/**
 * Build RR estimate block from precomputed job totals.
 * @param job
 * @returns {{parts: string, labor: string, total: string}|null}
 */
const buildEstimateFromJobTotals = (job) => {
  const totals = job?.job_totals;
  if (!totals) return null;

  const partsCents = toMoneyCents(totals?.parts?.parts?.total) + toMoneyCents(totals?.parts?.sublets?.total);
  const laborCents = toMoneyCents(totals?.rates?.rates_subtotal ?? totals?.rates?.subtotal);
  let totalCents = toMoneyCents(totals?.totals?.subtotal);

  if (!totalCents) {
    totalCents = partsCents + laborCents;
  }

  // If we truly have no numbers from totals, omit estimate entirely.
  if (!partsCents && !laborCents && !totalCents) return null;

  return {
    parts: asN2FromCents(partsCents),
    labor: asN2FromCents(laborCents),
    total: asN2FromCents(totalCents)
  };
};

/**
 * Build RR estimate block from the best available source.
 * @param job
 * @param allocations
 * @returns {{parts: string, labor: string, total: string}|null}
 */
const buildRREstimate = ({ job, allocations } = {}) => {
  return buildEstimateFromAllocations(allocations) || buildEstimateFromJobTotals(job);
};

/**
 * Build RO.GOG structure for the reynolds-rome-client `createRepairOrder` payload
 * from allocations.
 *
 * Supports the allocation shape:
 *   {
 *     center,
 *     partsSale,
 *     partsTaxableSale,
 *     partsNonTaxableSale,
 *     laborTaxableSale,
 *     laborNonTaxableSale,
 *     extrasSale,
 *     extrasTaxableSale,
 *     extrasNonTaxableSale,
 *     totalSale,
 *     cost,
 *     profitCenter,
 *     costCenter
 *   }
 *
 * For each center, we can emit up to 6 GOG *segments*:
 *   - taxable parts          (CustTxblNTxblFlag="T")
 *   - non-taxable parts      (CustTxblNTxblFlag="N")
 *   - taxable extras         (CustTxblNTxblFlag="T")
 *   - non-taxable extras     (CustTxblNTxblFlag="N")
 *   - taxable labor          (CustTxblNTxblFlag="T")
 *   - non-taxable labor      (CustTxblNTxblFlag="N")
 *
 * IMPORTANT:
 *   Each segment becomes its OWN JobNo / AllGogOpCodeInfo, with exactly one
 *   AllGogLineItmInfo inside. This keeps a clean 1:1 mapping between:
 *     - <AllGogOpCodeInfo> (ROGOG)
 *     - <OpCodeLaborInfo>  (ROLABOR)
 *   and ensures taxable/non-taxable flags line up by JobNo.
 *
 *   We attach segmentKind/segmentIndex/segmentCount metadata on each op
 *   for UI/debug purposes. The XML templates can safely ignore these.
 *
 * @param {Array} allocations
 * @param {Object} opts
 * @param {string} opts.opCode - RR OpCode for the job (global, overridable)
 * @param {string} [opts.payType="Cust"] - PayType (always "Cust" per Marc)
 * @param {string} [opts.roNo] - Optional RoNo to echo on <Rogog RoNo="">
 * @returns {null|{roNo?: string, ops: Array}}
 */
const buildRogogFromAllocations = (allocations, { opCode, payType = "Cust", roNo } = {}) => {
  if (!Array.isArray(allocations) || !allocations.length || !opCode) return null;

  const ops = [];

  const asMoneyLike = (amountCents) => ({
    amount: amountCents || 0,
    precision: 2
  });

  for (const alloc of allocations) {
    const pc = alloc?.profitCenter || {};
    const breakOut = pc.rr_gogcode;
    const itemType = pc.rr_item_type;
    const laborSideProfitCenter = isLaborSideProfitCenter(alloc);

    // Only centers configured for RR GOG are included
    if (!breakOut || !itemType) continue;

    const partsTaxableCents = toMoneyCents(alloc.partsTaxableSale);
    const partsNonTaxableCents = toMoneyCents(alloc.partsNonTaxableSale);
    const extrasTaxableCents = toMoneyCents(alloc.extrasTaxableSale);
    const extrasNonTaxableCents = toMoneyCents(alloc.extrasNonTaxableSale);
    const laborTaxableCents = toMoneyCents(alloc.laborTaxableSale);
    const laborNonTaxableCents = toMoneyCents(alloc.laborNonTaxableSale);
    const costCents = toMoneyCents(alloc.cost);

    const segments = [];

    // 1) Taxable parts segment -> "T"
    if (partsTaxableCents !== 0) {
      segments.push({
        kind: "partsTaxable",
        saleCents: partsTaxableCents,
        txFlag: "T"
      });
    }

    // 2) Non-taxable parts segment -> "N"
    if (partsNonTaxableCents !== 0) {
      segments.push({
        kind: "partsNonTaxable",
        saleCents: partsNonTaxableCents,
        txFlag: "N"
      });
    }

    // 3) Taxable extras -> "T"
    if (extrasTaxableCents !== 0) {
      segments.push({
        kind: "extrasTaxable",
        saleCents: extrasTaxableCents,
        txFlag: "T"
      });
    }

    // 4) Non-taxable extras -> "N"
    if (extrasNonTaxableCents !== 0) {
      segments.push({
        kind: "extrasNonTaxable",
        saleCents: extrasNonTaxableCents,
        txFlag: "N"
      });
    }

    // 5) Taxable labor segment -> "T"
    if (laborTaxableCents !== 0) {
      segments.push({
        kind: "laborTaxable",
        saleCents: laborTaxableCents,
        txFlag: "T"
      });
    }

    // 6) Non-tax labor segment -> "N"
    if (laborNonTaxableCents !== 0) {
      segments.push({
        kind: "laborNonTaxable",
        saleCents: laborNonTaxableCents,
        txFlag: "N"
      });
    }

    if (!segments.length) continue;

    // Proportionally split cost across segments based on their sale amounts
    const totalCostCents = costCents;
    const totalSaleCents = segments.reduce((sum, seg) => sum + seg.saleCents, 0);

    let remainingCostCents = totalCostCents;

    segments.forEach((seg, idx) => {
      let segCost = 0;

      if (totalCostCents > 0 && totalSaleCents > 0) {
        if (idx === segments.length - 1) {
          // Last segment gets the remainder to avoid rounding drift
          segCost = remainingCostCents;
        } else {
          segCost = Math.round((seg.saleCents / totalSaleCents) * totalCostCents);
          remainingCostCents -= segCost;
        }
      }

      seg.costCents = segCost;
    });

    const itemDescBase = pc.accountdesc || pc.accountname || alloc.center || "";
    const segmentCount = segments.length;

    // Each segment becomes its own op / JobNo with a single line
    segments.forEach((seg, idx) => {
      const jobNo = String(ops.length + 1); // global, 1-based JobNo across all centers/segments
      const isLaborSegment = seg.kind === "laborTaxable" || seg.kind === "laborNonTaxable";
      const isPartsSegment = seg.kind === "partsTaxable" || seg.kind === "partsNonTaxable";
      const rolaborRequired = isLaborSegment || (isPartsSegment && laborSideProfitCenter);
      const segmentHours = isLaborSegment
        ? seg.kind === "laborTaxable"
          ? toFiniteNumber(alloc.laborTaxableHours)
          : toFiniteNumber(alloc.laborNonTaxableHours)
        : 0;
      const segmentBillRate = isLaborSegment && segmentHours > 0 ? seg.saleCents / 100 / segmentHours : 0;

      const line = {
        breakOut,
        itemType,
        itemDesc: itemDescBase,
        custQty: "1.0",
        custPayTypeFlag: "C",
        custTxblNtxblFlag: seg.txFlag || "N",
        amount: {
          payType,
          amtType: "Unit",
          custPrice: asN2(asMoneyLike(seg.saleCents)),
          dlrCost: asN2(asMoneyLike(seg.costCents))
        }
      };

      ops.push({
        opCode,
        jobNo,
        lines: [line], // exactly one AllGogLineItmInfo per AllGogOpCodeInfo
        // Extra metadata for UI / debugging
        segmentKind: seg.kind,
        segmentIndex: idx,
        segmentCount,
        segmentHours,
        segmentBillRate,
        rolaborRequired
      });
    });
  }

  if (!ops.length) return null;

  return {
    roNo,
    ops
  };
};

/**
 * Build RO.ROLABOR structure for the reynolds-rome-client `createRepairOrder` payload
 * from an already-built RO.GOG structure.
 *
 * We still keep a 1:1 mapping with GOG ops: each op gets a corresponding
 * OpCodeLaborInfo entry using the same JobNo and the same tax flag as its
 * GOG line. Sale amounts are mirrored into ROLABOR so Reynolds has a
 * non-zero job anchor for every ROGOG JobNo; when labor hours are available
 * from allocations, weighted bill hours/rates are also populated.
 *
 * @param {Object} rogg - result of buildRogogFromAllocations
 * @param {Object} opts
 * @param {string} [opts.payType="Cust"]
 * @returns {null|{ops: Array}}
 */
const buildRolaborFromRogog = (rogg, { payType = "Cust" } = {}) => {
  if (!rogg || !Array.isArray(rogg.ops)) return null;

  const ops = rogg.ops.map((op) => {
    const firstLine = op.lines?.[0] || {};

    // Pull tax flag from the GOG line.
    // Prefer the property we set in buildRogogFromAllocations (custTxblNTxblFlag),
    // but also accept custTxblNtxblFlag in case we ever change naming.
    const txFlag = firstLine.custTxblNtxblFlag ?? "N";

    const linePayType = firstLine.custPayTypeFlag || "C";
    const isLaborSegment = op.segmentKind === "laborTaxable" || op.segmentKind === "laborNonTaxable";
    const laborAmount = String(firstLine?.amount?.custPrice ?? "0");
    const laborBill = isLaborSegment
      ? buildRolaborBillFields({
          amountUnits: laborAmount,
          hours: op.segmentHours,
          rate: op.segmentBillRate
        })
      : {
          jobTotalHrs: "0",
          billTime: "0",
          billRate: "0"
        };

    return {
      opCode: op.opCode,
      jobNo: op.jobNo,
      custPayTypeFlag: linePayType,
      custTxblNtxblFlag: txFlag,
      bill: {
        payType,
        ...laborBill
      },
      amount: {
        payType,
        amtType: "Job",
        custPrice: laborAmount,
        totalAmt: laborAmount
      }
    };
  });

  if (!ops.length) return null;

  return { ops };
};

/**
 * Query job data by ID from GraphQL API.
 * @param ctx
 * @param jobId
 * @returns {Promise<*>}
 * @constructor
 */
const QueryJobData = async (ctx = {}, jobId) => {
  if (ctx?.job) return ctx.job;
  if (ctx?.payload?.job) return ctx.payload.job;

  const id = pickJobId(ctx, jobId);
  if (!id) throw new Error("QueryJobData: jobId required (none found in ctx or args)");

  try {
    const res = await client.request(GET_JOB_BY_PK, { id });
    const job = res?.jobs_by_pk;
    if (!job) throw new Error(`Job ${id} not found`);
    return job;
  } catch (e) {
    const msg = e?.response?.errors?.[0]?.message || e.message || "unknown";
    throw new Error(`QueryJobData failed: ${msg}`);
  }
};

/**
 * Build Repair Order payload for RR from job and customer data.
 * @param {Object} args
 * @param job
 * @param selectedCustomer
 * @param advisorNo
 * @param story
 * @param makeOverride
 * @param allocations
 * @param {string} [opCode]   - RR OpCode for this RO (global default / override)
 * @returns {Object}
 */
const buildRRRepairOrderPayload = ({
  job,
  selectedCustomer,
  advisorNo,
  story,
  makeOverride,
  allocations,
  opCode
} = {}) => {
  const customerNo = selectedCustomer?.customerNo
    ? String(selectedCustomer.customerNo).trim()
    : selectedCustomer?.custNo
      ? String(selectedCustomer.custNo).trim()
      : null;

  if (!customerNo) throw new Error("No RR customer selected (customerNo/custNo missing)");

  const adv = advisorNo != null && String(advisorNo).trim() !== "" ? String(advisorNo).trim() : null;
  if (!adv) throw new Error("advisorNo is required for RR export");

  const vinRaw = job?.v_vin;
  const vin =
    typeof vinRaw === "string"
      ? vinRaw
          .replace(/[^A-Za-z0-9]/g, "")
          .toUpperCase()
          .slice(0, 17) || undefined
      : undefined;

  // Use ro_number when present; fallback to job.id
  const ro = job?.ro_number != null ? job.ro_number : job?.id != null ? job.id : null;
  if (ro == null) throw new Error("Missing repair order identifier (ro_number/id)");

  const roStr = String(ro);

  // Base payload shape expected by reynolds-rome-client (buildCreateRepairOrder)
  const payload = {
    outsdRoNo: roStr,
    departmentType: "B",
    vin,
    customerNo: String(customerNo),
    advisorNo: adv,
    mileageIn: job.kmin
  };

  const estimate = buildRREstimate({ job, allocations });
  if (estimate) {
    payload.estimate = estimate;
  }

  if (story) {
    payload.roComment = String(story).trim();
  }

  if (makeOverride) {
    // Passed through so the template can override DMS Make if needed
    payload.makeOverride = String(makeOverride).trim();
  }

  const haveAllocations = Array.isArray(allocations) && allocations.length > 0;

  if (haveAllocations) {
    const effectiveOpCode = (opCode && String(opCode).trim()) || null;

    if (effectiveOpCode) {
      // Build RO.GOG and RO.LABOR in the new normalized shape
      const rogg = buildRogogFromAllocations(allocations, {
        opCode: effectiveOpCode,
        payType: "Cust"
      });

      if (rogg) {
        payload.rogg = rogg;

        const rolabor = buildRolaborFromRogog(rogg, { payType: "Cust" });
        if (rolabor) {
          payload.rolabor = rolabor;
        }
      }
    }
  }

  return payload;
};

/**
 * Make vehicle search payload from job data
 * @param job
 * @returns {{kind: string, license: string}|null|{kind: string, vin: *|string}}
 */
const makeVehicleSearchPayloadFromJob = (job) => {
  const vin = safeVin(job);
  if (vin) return { kind: "vin", vin };

  const plate = job?.plate_no;
  if (plate) return { kind: "license", license: String(plate).trim() };

  return null;
};

/**
 * Make customer search payload from job data
 * @param job
 * @returns {{kind: string, vin: *|string}|{kind: string, name: {name: string}}|{kind: string, phone: string}|null}
 */
const makeCustomerSearchPayloadFromJob = (job) => {
  const phone = job?.ownr_ph1;
  const d = digitsOnly(phone);
  if (d.length >= 7) return { kind: "phone", phone: d };

  const lastName = job?.ownr_ln;
  const company = job?.ownr_co_nm;
  const lnOrCompany = lastName || company;
  if (lnOrCompany) return { kind: "name", name: { name: String(lnOrCompany).trim() } };

  const vin = safeVin(job);
  if (vin) return { kind: "vin", vin };

  return null;
};

/**
 * Normalize customer candidates from combined search result.
 * @param res
 * @returns {*[]}
 */
const normalizeCustomerCandidates = (res) => {
  const blocks = blocksFromCombinedSearchResult(res);
  const out = [];
  for (const blk of blocks) {
    const serv = Array.isArray(blk?.ServVehicle) ? blk.ServVehicle : [];
    const custNos = serv.map((sv) => sv?.VehicleServInfo?.CustomerNo).filter(Boolean);

    const nci = blk?.NameContactId;
    const ind = nci?.NameId?.IndName;
    const bus = nci?.NameId?.BusName;
    const personal = [ind?.FName, ind?.LName].filter(Boolean).join(" ").trim();
    const company = bus?.CompanyName;
    const name = (personal || company || "").trim();

    for (const custNo of custNos) {
      out.push({ custNo, name: name || `Customer ${custNo}`, _blk: blk });
    }
  }
  const seen = new Set();
  return out.filter((c) => {
    if (!c.custNo || seen.has(c.custNo)) return false;
    seen.add(c.custNo);
    return true;
  });
};

/**
 * Normalize vehicle candidates from combined search result.
 * @param res
 * @returns {*[]}
 */
const normalizeVehicleCandidates = (res) => {
  const blocks = blocksFromCombinedSearchResult(res);
  const out = [];
  for (const blk of blocks) {
    const serv = Array.isArray(blk?.ServVehicle) ? blk.ServVehicle : [];
    for (const sv of serv) {
      const v = sv?.Vehicle || {};
      const vin = v?.Vin || v?.VIN || v?.vin;

      if (!vin) continue;

      const year = v?.VehicleYr || v?.ModelYear || v?.Year;
      const make = v?.VehicleMake || v?.MakeName || v?.Make;
      const model = v?.MdlNo || v?.ModelDesc || v?.Model;

      const label = [year, make, model, vin].filter(Boolean).join(" ");
      out.push({ vin, year, make, model, label, _blk: blk });
    }
  }
  const seen = new Set();
  return out.filter((v) => {
    if (!v.vin || seen.has(v.vin)) return false;
    seen.add(v.vin);
    return true;
  });
};

/**
 * Build split labor lines from job allocations.
 * @param jobAllocations
 * @returns {*[]}
 */
const buildSplitLaborLinesFromAllocations = (jobAllocations) => {
  const lines = [];

  for (const alloc of jobAllocations || []) {
    const { center, laborTaxableSale, laborNonTaxableSale, profitCenter, costCenter } = alloc;

    // Taxable labor
    if (laborTaxableSale && !laborTaxableSale.isZero()) {
      lines.push({
        centerName: center,
        profitCenter,
        costCenter,
        amount: laborTaxableSale,
        isTaxable: true,
        source: "labor"
      });
    }

    // Non-taxable labor
    if (laborNonTaxableSale && !laborNonTaxableSale.isZero()) {
      lines.push({
        centerName: center,
        profitCenter,
        costCenter,
        amount: laborNonTaxableSale,
        isTaxable: false,
        source: "labor"
      });
    }
  }

  return lines;
};

module.exports = {
  QueryJobData,
  buildRRRepairOrderPayload,
  makeCustomerSearchPayloadFromJob,
  buildSplitLaborLinesFromAllocations,
  makeVehicleSearchPayloadFromJob,
  normalizeCustomerCandidates,
  normalizeVehicleCandidates,
  buildRogogFromAllocations,
  buildRolaborFromRogog,
  buildMinimalRolaborFromJob
};
