const client = require("../../../graphql-client/graphql-client").client;
const { extractPartsTaxRates } = require("./lib/extractPartsTaxRates");
const { parseXml, normalizeXmlObject } = require("../partsManagementUtils");
const opCodes = require("./lib/opCodes.json");
// New imports for S3 XML archival
const { uploadFileToS3 } = require("../../../utils/s3");
const InstanceMgr = require("../../../utils/instanceMgr").default;

// GraphQL Queries and Mutations
const {
  GET_BODYSHOP_STATUS,
  GET_VEHICLE_BY_SHOP_VIN,
  INSERT_OWNER,
  INSERT_JOB_WITH_LINES
} = require("../partsManagement.queries");
const { v4: uuidv4 } = require("uuid");

// Defaults
const FALLBACK_DEFAULT_JOB_STATUS = "Open";

const ESTIMATE_XML_BUCKET =
  process.env?.NODE_ENV === "development"
    ? "parts-estimates" // local/dev shared bucket name
    : InstanceMgr({
        imex: `imex-webest-xml`,
        rome: `rome-webest-xml`
      });

const buildEstimateXmlKey = (rq) => {
  const refClaimNum = rq.RefClaimNum;
  const shopId = rq.ShopID;

  const ts = new Date().toISOString().replace(/:/g, "-");
  const safeClaim = (refClaimNum || "no-claim").toString().replace(/[^A-Za-z0-9_-]/g, "_");
  return `addRequest/${shopId}/${safeClaim}/${ts}-${uuidv4()}.xml`;
};

/**
 * Fetches the default order status for a bodyshop.
 * @param {string} shopId - The bodyshop UUID.
 * @param {object} logger - The logger instance.
 * @returns {Promise<string>} The default status or fallback.
 */
const getDefaultJobStatus = async (shopId, logger) => {
  try {
    const { bodyshop_by_pk } = await client.request(GET_BODYSHOP_STATUS, { id: shopId });
    return bodyshop_by_pk?.md_ro_statuses?.default_imported || FALLBACK_DEFAULT_JOB_STATUS;
  } catch (err) {
    logger.log("parts-bodyshop-fetch-failed", "warn", shopId, null, { error: err });
    return FALLBACK_DEFAULT_JOB_STATUS;
  }
};
/**
 * Finds an existing vehicle by shopId and VIN.
 * @param {string} shopId - The bodyshop UUID.
 * @param {string} v_vin - The vehicle VIN.
 * @param {object} logger - The logger instance.
 * @returns {Promise<string|null>} The vehicle ID or null if not found.
 */
const findExistingVehicle = async (shopId, v_vin, logger) => {
  if (!v_vin) return null;

  try {
    const { vehicles } = await client.request(GET_VEHICLE_BY_SHOP_VIN, { shopid: shopId, v_vin });
    if (vehicles?.length > 0) {
      logger.log("parts-vehicle-found", "info", vehicles[0].id, null, { shopid: shopId, v_vin });
      return vehicles[0].id;
    }
  } catch (err) {
    logger.log("parts-vehicle-fetch-failed", "warn", null, null, { error: err });
  }
  return null;
};

/**
 * Extracts job-related data from the XML request.
 * @param {object} rq - The VehicleDamageEstimateAddRq object.
 * @returns {object} Extracted job data.
 */
const extractJobData = (rq) => {
  const doc = rq.DocumentInfo || {};
  const ev = rq.EventInfo || {};
  const asgn = ev.AssignmentEvent || {};
  const ci = rq.ClaimInfo || {};

  return {
    driveable: !!rq.VehicleInfo?.Condition?.DrivableInd,
    shopId: rq.ShopID || rq.shopId,
    //     status: ci.ClaimStatus || null, Proper, setting it default for now
    refClaimNum: rq.RefClaimNum,
    ciecaid: rq.RqUID || null,
    // Pull Cieca_ttl from ClaimInfo per schema/sample
    cieca_ttl: parseFloat(ci.Cieca_ttl || 0),
    cat_no: doc.VendorCode || null,
    category: doc.DocumentType || null,
    classType: doc.DocumentStatus || null,
    comment: doc.Comment || null,
    asgn_no: asgn.AssignmentNumber || null,
    asgn_type: asgn.AssignmentType || null,
    asgn_date: asgn.AssignmentDate || null,
    // asgn_created: asgn.CreateDateTime || null,
    scheduled_in: ev.RepairEvent?.RequestedPickUpDateTime || null,
    scheduled_completion: ev.RepairEvent?.TargetCompletionDateTime || null,
    clm_no: ci.ClaimNum || null,
    policy_no: ci.PolicyInfo?.PolicyInfo?.PolicyNum || ci.PolicyInfo?.PolicyNum || null,
    ded_amt: parseFloat(ci.PolicyInfo?.CoverageInfo?.Coverage?.DeductibleInfo?.DeductibleAmt || 0)
  };
};

/**
 * Extracts owner data from the XML request.
 * Falls back to Claimant if Owner is missing.
 * @param {object} rq - The VehicleDamageEstimateAddRq object.
 * @param {string} shopId - The bodyshop UUID.
 * @returns {object} Owner data for insertion and inline use.
 */
const extractOwnerData = (rq, shopId) => {
  const ownerOrClaimant = rq.AdminInfo?.Owner?.Party || rq.AdminInfo?.Claimant?.Party || {};
  const personInfo = ownerOrClaimant.PersonInfo || {};
  const personName = personInfo.PersonName || {};
  const address = personInfo.Communications?.Address || {};

  let ownr_ph1, ownr_ph2, ownr_ea;

  const comms = Array.isArray(ownerOrClaimant.ContactInfo?.Communications)
    ? ownerOrClaimant.ContactInfo.Communications
    : [ownerOrClaimant.ContactInfo?.Communications || {}];

  for (const c of comms) {
    // --  Document
    if (c.CommQualifier === "CP") ownr_ph1 = c.CommPhone;
    if (c.CommQualifier === "WP") ownr_ph2 = c.CommPhone;
    if (c.CommQualifier === "EM") ownr_ea = c.CommEmail;
    // if (c.CommQualifier === "AL") ownr_alt_ph = c.CommPhone;
  }

  return {
    shopid: shopId,
    ownr_fn: personName.FirstName || null,
    ownr_ln: personName.LastName || null,
    ownr_co_nm: ownerOrClaimant.OrgInfo?.CompanyName || null,
    ownr_addr1: address.Address1 || null,
    ownr_addr2: address.Address2 || null,
    ownr_city: address.City || null,
    ownr_st: address.StateProvince || null,
    ownr_zip: address.PostalCode || null,
    ownr_ctry: address.Country || null,
    ownr_ph1,
    ownr_ph2,
    ownr_ea
    // ownr_alt_ph
    // ownr_id_qualifier: ownerOrClaimant.IDInfo?.IDQualifierCode || null // New
    // ownr_id_num: ownerOrClaimant.IDInfo?.IDNum || null, // New
    // ownr_preferred_contact: ownerOrClaimant.PreferredContactMethod || null // New
  };
};

/**
 * Extracts estimator data from the XML request.
 * @param {object} rq - The VehicleDamageEstimateAddRq object.
 * @returns {object} Estimator data.
 */
const extractEstimatorData = (rq) => {
  const estParty = rq.AdminInfo?.Estimator?.Party || {};
  const estComms = Array.isArray(estParty.ContactInfo?.Communications)
    ? estParty.ContactInfo.Communications
    : [estParty.ContactInfo?.Communications || {}];

  return {
    est_co_nm: rq.AdminInfo?.Estimator?.Affiliation || null,
    est_ct_fn: estParty.PersonInfo?.PersonName?.FirstName || null,
    est_ct_ln: estParty.PersonInfo?.PersonName?.LastName || null,
    est_ea: estComms.find((c) => c.CommQualifier === "EM")?.CommEmail || null
  };
};

/**
 * Extracts adjuster data from the XML request.
 * @param {object} rq - The VehicleDamageEstimateAddRq object.
 * @returns {object} Adjuster data.
 */
// const extractAdjusterData = (rq) => {
//   const adjParty = rq.AdminInfo?.Adjuster?.Party || {};
//   const adjComms = Array.isArray(adjParty.ContactInfo?.Communications)
//     ? adjParty.ContactInfo.Communications
//     : [adjParty.ContactInfo?.Communications || {}];
//
//   return {
//     //TODO (FUTURE): I dont think we display agt_ct_* fields in app. Have they typically been sending data here?
//     agt_ct_fn: adjParty.PersonInfo?.PersonName?.FirstName || null,
//     agt_ct_ln: adjParty.PersonInfo?.PersonName?.LastName || null,
//     agt_ct_ph: adjComms.find((c) => c.CommQualifier === "CP")?.CommPhone || null,
//     agt_ea: adjComms.find((c) => c.CommQualifier === "EM")?.CommEmail || null
//   };
// };

/**
 * Extracts repair facility data from the XML request.
 * @param {object} rq - The VehicleDamageEstimateAddRq object.
 * @returns {object} Repair facility data.
 */
// const extractRepairFacilityData = (rq) => {
//   const rfParty = rq.AdminInfo?.RepairFacility?.Party || {};
//   const rfComms = Array.isArray(rfParty.ContactInfo?.Communications)
//     ? rfParty.ContactInfo.Communications
//     : [rfParty.ContactInfo?.Communications || {}];
//
//   return {
//     servicing_dealer: rfParty.OrgInfo?.CompanyName || null,
//     // TODO (Future):  The servicing dealer fields are a relic from synergy for a few folks
//     // TODO (Future):  I suspect RF data could be ignored since they are the RF.
//     servicing_dealer_contact:
//       rfComms.find((c) => c.CommQualifier === "WP" || c.CommQualifier === "FX")?.CommPhone || null
//   };
// };

/**
 * Extracts loss information from the XML request.
 * @param rq
 * @returns {{loss_dt: (*|null), reported_dt: (*|null), loss_type_code: (*|null), loss_type_desc: (*|null)}}
 */
const extractLossInfo = (rq) => {
  const loss = rq.ClaimInfo?.LossInfo?.Facts || {};
  const custom = rq.ClaimInfo?.CustomElement || {};
  return {
    loss_date: loss.LossDateTime || null,
    loss_type: custom.LossTypeCode || null,
    loss_desc: custom.LossTypeDesc || null
    // area_of_impact: {
    //   impact_1: loss.PrimaryPOI?.POICode || null,
    //   imact_2 :loss.SecondaryPOI?.POICode || null,
    // },
    // tlosind:  rq.ClaimInfo?.LossInfo?.TotalLossInd || null,
    // damage_memo: loss.DamageMemo || null, //(maybe ins_memo)
  };
};

/**
 * Extracts insurance data from the XML request.
 * @param rq
 * @returns {{insd_ln: (*|null), insd_fn: (string|null), insd_title: (*|null), insd_co_nm: (*|string|null), insd_addr1:
 * (*|null), insd_addr2: (*|null), insd_city: (*|null), insd_st: (*|null), insd_zip: (*|null), insd_ctry: (*|null),
 * insd_ph1, insd_ph1x, insd_ph2, insd_ph2x, insd_fax, insd_faxx, insd_ea}}
 */
const extractInsuranceData = (rq) => {
  const insuredParty = rq.AdminInfo?.Insured?.Party || {};
  const insuredPerson = insuredParty.PersonInfo || {};
  const insuredComms = Array.isArray(insuredParty.ContactInfo?.Communications)
    ? insuredParty.ContactInfo.Communications
    : [insuredParty.ContactInfo?.Communications || {}];
  const insuredAddress = insuredPerson.Communications?.Address || {};

  const insurerParty = rq.AdminInfo?.InsuranceCompany?.Party || {};

  let insd_ph1, insd_ph1x, insd_ph2, insd_ph2x, insd_fax, insd_faxx, insd_ea;

  for (const c of insuredComms) {
    if (c.CommQualifier === "CP") {
      insd_ph1 = c.CommPhone;
      insd_ph1x = c.CommPhoneExt;
    }
    if (c.CommQualifier === "WP") {
      insd_ph2 = c.CommPhone;
      insd_ph2x = c.CommPhoneExt;
    }
    if (c.CommQualifier === "FX") {
      insd_fax = c.CommPhone;
      insd_faxx = c.CommPhoneExt;
    }
    if (c.CommQualifier === "EM") insd_ea = c.CommEmail;
  }

  return {
    insd_ln: insuredPerson.PersonName?.LastName || null,
    insd_fn: insuredPerson.PersonName?.FirstName || null,
    insd_title: insuredPerson.PersonName?.Title || null,
    insd_co_nm: insurerParty.OrgInfo?.CompanyName || insuredParty.OrgInfo?.CompanyName || null,
    insd_addr1: insuredAddress.Address1 || null,
    insd_addr2: insuredAddress.Address2 || null,
    insd_city: insuredAddress.City || null,
    insd_st: insuredAddress.StateProvince || null,
    insd_zip: insuredAddress.PostalCode || null,
    insd_ctry: insuredAddress.Country || null,
    insd_ph1,
    insd_ph1x,
    insd_ph2,
    insd_ph2x,
    insd_fax,
    insd_faxx,
    insd_ea
  };
};

/**
 * Extracts vehicle data from the XML request.
 * @param {object} rq - The VehicleDamageEstimateAddRq object.
 * @param {string} shopId - The bodyshop UUID.
 * @returns {object} Vehicle data for insertion and inline use.
 */
const extractVehicleData = (rq, shopId) => {
  const desc = rq.VehicleInfo?.VehicleDesc || {};
  const exterior = rq.VehicleInfo?.Paint?.Exterior || {};
  const interior = rq.VehicleInfo?.Paint?.Interior || {};
  return {
    shopid: shopId,
    // VIN may be either VINInfo.VINNum or VINInfo.VIN.VINNum depending on producer
    v_vin: rq.VehicleInfo?.VINInfo?.VINNum || rq.VehicleInfo?.VINInfo?.VIN?.VINNum || null,
    plate_no: rq.VehicleInfo?.License?.LicensePlateNum || null,
    plate_st: rq.VehicleInfo?.License?.LicensePlateStateProvince || null,
    v_model_yr: desc.ModelYear || null,
    v_make_desc: desc.MakeDesc || null,
    v_model_desc: desc.ModelName || null,
    v_color: exterior.Color?.ColorName || null,
    v_bstyle: desc.BodyStyle || null,
    v_engine: desc.EngineDesc || null,
    // TODO (for future) Need to confirm with exact data, but this is typically a list of options. Not used AFAIK.
    // v_options: desc.SubModelDesc || null,
    v_type: desc.FuelType || null,
    v_cond: rq.VehicleInfo?.Condition?.DrivableInd,
    v_trimcode: desc.TrimCode || null,
    v_tone: exterior.Tone || null,
    v_stage: exterior.RefinishStage || rq.VehicleInfo?.Paint?.RefinishStage || null,
    v_prod_dt: desc.ProductionDate || null,
    v_paint_codes: Array.isArray(exterior.PaintCodeInfo)
      ? exterior.PaintCodeInfo.map((p) => p.PaintCode).join(",")
      : exterior.PaintCode || null,
    v_mldgcode: desc.MldgCode || null,
    v_makecode: desc.MakeCode || null,
    trim_color: interior.ColorName || desc.TrimColor || null,
    db_v_code: desc.DatabaseCode || null
  };
};

/**
 * Extracts job lines from the XML request.
 * @param {object} rq - The VehicleDamageEstimateAddRq object.
 * @returns {object[]} Array of job line objects.
 */
const extractJobLines = (rq) => {
  // Normalize to array without lodash toArray (which flattens object values incorrectly)
  const dl = rq.DamageLineInfo;
  const damageLines = Array.isArray(dl) ? dl : dl ? [dl] : [];
  if (damageLines.length === 0) {
    return [];
  }

  const out = [];

  for (const line of damageLines) {
    const partInfo = line.PartInfo || {};
    const laborInfo = line.LaborInfo || {};
    const refinishInfo = line.RefinishLaborInfo || {};
    const subletInfo = line.SubletInfo || {};

    const base = {
      line_no: parseInt(line.LineNum || 0, 10),
      unq_seq: parseInt(line.UniqueSequenceNum || 0, 10),
      status: line.LineStatusCode || null,
      line_desc: line.LineDesc || null,
      notes: line.LineMemo || null
    };

    const lineOut = { ...base };

    // Manual line flag coercion
    // if (line.ManualLineInd !== undefined) {
    //   lineOut.manual_line =
    //     line.ManualLineInd === true ||
    //     line.ManualLineInd === 1 ||
    //     line.ManualLineInd === "1" ||
    //     // TODO (FUTURE): manual line tracks manual in IO or not, this woudl presumably always be false
    //     (typeof line.ManualLineInd === "string" && line.ManualLineInd.toUpperCase() === "Y");
    // } else {
    //   lineOut.manual_line = null;
    // }

    // Is set to false because anything coming from the DMS is considered not a manual line, it becomes
    // a manual line once it is edited in OUR system.
    lineOut.manual_line = false;

    // Parts (preferred) or Sublet (fallback when no PartInfo)
    const hasPart = Object.keys(partInfo).length > 0;
    const hasSublet = Object.keys(subletInfo).length > 0;

    if (hasPart) {
      lineOut.part_type = partInfo.PartType || null ? String(partInfo.PartType).toUpperCase() : null;
      lineOut.part_qty = parseFloat(partInfo.Quantity || 0) || 1;
      lineOut.oem_partno = partInfo.OEMPartNum;
      lineOut.alt_partno = partInfo?.NonOEM?.NonOEMPartNum;

      // THIS NEEDS TO BE CHANGED IN CHANGE REQUEST
      lineOut.act_price = parseFloat(partInfo?.PartPrice || 0);
      lineOut.db_price = parseFloat(partInfo?.OEMPartPrice || 0);

      // Tax flag from PartInfo.TaxableInd when provided
      if (
        partInfo.TaxableInd !== undefined &&
        (typeof partInfo.TaxableInd === "string" ||
          typeof partInfo.TaxableInd === "number" ||
          typeof partInfo.TaxableInd === "boolean")
      ) {
        lineOut.tax_part =
          partInfo.TaxableInd === true ||
          partInfo.TaxableInd === 1 ||
          partInfo.TaxableInd === "1" ||
          (typeof partInfo.TaxableInd === "string" && partInfo.TaxableInd.toUpperCase() === "Y");
      }
    }

    //TODO (FUTURE): Some nuance here. Usually a part and sublet amount shouldnt be on the same line, but they theoretically
    // could. May require additional discussion.
    // EMS - > Misc Amount, calibration for example, painting, etc
    else if (hasSublet) {
      const amt = parseFloat(subletInfo.SubletAmount || 0);
      lineOut.part_type = "PAS"; // Sublet as parts-as-service
      lineOut.part_qty = 1;
      lineOut.act_price = isNaN(amt) ? 0 : amt;
    }

    // Primary labor (if present) recorded on the same line
    const hrs = parseFloat(laborInfo.LaborHours || 0);
    const amt = parseFloat(laborInfo.LaborAmt || 0);
    const hasLabor =
      (!!laborInfo.LaborType && String(laborInfo.LaborType).length > 0) ||
      (!isNaN(hrs) && hrs !== 0) ||
      (!isNaN(amt) && amt !== 0);

    if (hasLabor) {
      lineOut.mod_lbr_ty = laborInfo.LaborType || null;
      lineOut.mod_lb_hrs = isNaN(hrs) ? 0 : hrs;
      const opCodeKey =
        typeof laborInfo.LaborOperation === "string" ? laborInfo.LaborOperation.trim().toUpperCase() : null;
      lineOut.op_code_desc = opCodes?.[opCodeKey]?.desc || null;

      lineOut.lbr_amt = isNaN(amt) ? 0 : amt;
    }

    //TODO (FUTURE):  what's the BMS logic for this? Body and refinish operations can often happen to the same part,
    // but most systems output a second line for the refinish labor.
    //TODO (FUTURE): 2nd line may include a duplicate of the part price, but that can be removed. This is the case for CCC.
    // Refinish labor (if present) recorded on the same line using secondary labor fields

    const rHrs = parseFloat(refinishInfo.LaborHours || 0);
    const rAmt = parseFloat(refinishInfo.LaborAmt || 0);
    const hasRefinish =
      Object.keys(refinishInfo).length > 0 &&
      ((refinishInfo.LaborType && String(refinishInfo.LaborType).length > 0) ||
        !isNaN(rHrs) ||
        !isNaN(rAmt) ||
        !!refinishInfo.LaborOperation);
    if (hasRefinish) {
      lineOut.lbr_typ_j = !!refinishInfo?.LaborAmtJudgmentInd;
      lineOut.lbr_hrs_j = !!refinishInfo?.LaborHoursJudgmentInd;
      lineOut.lbr_op_j = !!refinishInfo.LaborOperationJudgmentInd;
      // Aggregate refinish labor amount into the total labor amount for the line
      if (!isNaN(rAmt)) {
        lineOut.lbr_amt = (Number.isFinite(lineOut.lbr_amt) ? lineOut.lbr_amt : 0) + rAmt;
      }
      if (refinishInfo.PaintStagesNum !== undefined) lineOut.paint_stg = refinishInfo.PaintStagesNum;
      if (refinishInfo.PaintTonesNum !== undefined) lineOut.paint_tone = refinishInfo.PaintTonesNum;
    }

    out.push(lineOut);
  }

  return out;
};

// Helper to extract a GRAND TOTAL amount from RepairTotalsInfo
// const extractGrandTotal = (rq) => {
//   const rti = rq.RepairTotalsInfo;
//   const groups = Array.isArray(rti) ? rti : rti ? [rti] : [];
//   for (const grp of groups) {
//     const sums = Array.isArray(grp.SummaryTotalsInfo)
//       ? grp.SummaryTotalsInfo
//       : grp.SummaryTotalsInfo
//         ? [grp.SummaryTotalsInfo]
//         : [];
//     for (const s of sums) {
//       const type = (s.TotalType || "").toString().toUpperCase();
//       const desc = (s.TotalTypeDesc || "").toString().toUpperCase();
//       if (type.includes("GRAND") || type === "TOTAL" || desc.includes("GRAND")) {
//         const amt = parseFloat(s.TotalAmt ?? "NaN");
//         if (!isNaN(amt)) return amt;
//       }
//     }
//   }
//   return null;
// };

/**
 * Inserts an owner and returns the owner ID.
 * @param {object} ownerInput - The owner data to insert.
 * @param {object} logger - The logger instance.
 * @returns {Promise<string|null>} The owner ID or null if insertion fails.
 */
const insertOwner = async (ownerInput, logger) => {
  try {
    const { insert_owners_one } = await client.request(INSERT_OWNER, { owner: ownerInput });
    return insert_owners_one?.id;
  } catch (err) {
    logger.log("parts-owner-insert-failed", "warn", null, null, { error: err });
    return null;
  }
};

// Fallback: compute a naive total from joblines (parts + sublet + labor amounts)
// const computeLinesTotal = (joblines = []) => {
//   let parts = 0;
//   let labor = 0;
//   for (const jl of joblines) {
//     if (jl?.part_type) {
//       const qty = Number.isFinite(jl.part_qty) ? jl.part_qty : 1;
//       const price = Number.isFinite(jl.act_price) ? jl.act_price : 0;
//       parts += price * (qty || 1);
//     } else if (!jl.part_type && Number.isFinite(jl.act_price)) {
//       parts += jl.act_price;
//     }
//     if (Number.isFinite(jl.lbr_amt)) {
//       labor += jl.lbr_amt;
//     }
//   }
//   const total = parts + labor;
//
//   //TODO (FUTURE): clm_total is the 100% full amount of the repair including deductible, betterment and taxes. Typically provided by the source system.
//   return Number.isFinite(total) && total > 0 ? total : 0;
//   //TODO (FUTURE): clm_total is the 100% full amount of the repair including deductible,
//   // betterment and taxes. Typically provided by the source system.

/**
 * Handles the VehicleDamageEstimateAddRq XML request from parts management.
 * @param {object} req - The HTTP request object.
 * @param {object} res - The HTTP response object.
 * @returns {Promise<void>}
 */
const vehicleDamageEstimateAddRq = async (req, res) => {
  const { logger } = req;
  const rawXml = typeof req.body === "string" ? req.body : Buffer.isBuffer(req.body) ? req.body.toString("utf8") : "";
  try {
    const payload = await parseXml(req.body, logger);
    const rq = normalizeXmlObject(payload.VehicleDamageEstimateAddRq);
    const {
      shopId,
      refClaimNum,
      ciecaid,
      cieca_ttl,
      cat_no,
      category,
      classType,
      comment,
      date_exported,
      asgn_no,
      asgn_type,
      asgn_date,
      scheduled_in,
      scheduled_completion,
      clm_no,
      policy_no,
      ded_amt,
      driveable
    } = extractJobData(rq);
    const defaultStatus = await getDefaultJobStatus(shopId, logger);
    const parts_tax_rates = extractPartsTaxRates(rq.ProfileInfo);
    const ownerData = extractOwnerData(rq, shopId);
    const estimatorData = extractEstimatorData(rq);
    const vehicleData = extractVehicleData(rq, shopId);
    const lossInfo = extractLossInfo(rq);
    const joblinesData = extractJobLines(rq);
    const insuranceData = extractInsuranceData(rq);
    const ownerid = await insertOwner(ownerData, logger);
    const vehicleid = await findExistingVehicle(shopId, vehicleData.v_vin, logger);
    const jobInput = {
      shopid: shopId,
      driveable,
      converted: true,
      ownerid,
      ro_number: refClaimNum,
      ciecaid,
      cieca_ttl,
      cat_no,
      category,
      class: classType,
      parts_tax_rates,
      clm_no,
      status: defaultStatus,
      clm_total: 0,
      policy_no,
      ded_amt,
      comment,
      date_exported,
      asgn_no,
      asgn_type,
      asgn_date,
      scheduled_in,
      scheduled_completion,
      ...insuranceData,
      ...lossInfo,
      ...ownerData,
      ...estimatorData,
      v_vin: vehicleData.v_vin,
      v_model_yr: vehicleData.v_model_yr,
      v_model_desc: vehicleData.v_model_desc,
      v_make_desc: vehicleData.v_make_desc,
      v_color: vehicleData.v_color,
      plate_no: vehicleData.plate_no,
      plate_st: vehicleData.plate_st,
      ...(vehicleid ? { vehicleid } : { vehicle: { data: vehicleData } }),
      joblines: { data: joblinesData }
    };
    const { insert_jobs_one: newJob } = await client.request(INSERT_JOB_WITH_LINES, { job: jobInput });

    // Upload AFTER job creation to include job id in filename
    (async () => {
      try {
        const key = buildEstimateXmlKey(rq);
        await uploadFileToS3({
          bucketName: ESTIMATE_XML_BUCKET,
          key,
          content: rawXml || "",
          contentType: "application/xml"
        });
        logger.log("parts-estimate-xml-uploaded", "info", shopId, newJob.id, { key, bytes: rawXml?.length || 0 });
      } catch (e) {
        logger.log("parts-estimate-xml-upload-failed", "warn", shopId, null, { error: e?.message });
      }
    })();
    return res.status(200).json({ success: true, jobId: newJob.id });
  } catch (err) {
    logger.log("parts-route-error", "error", null, null, { error: err });
    return res.status(err.status || 500).json({ error: err.message || "Internal error" });
  }
};

module.exports = vehicleDamageEstimateAddRq;
