const queries = require("../graphql-client/queries");
const Dinero = require("dinero.js");
const moment = require("moment-timezone");
var builder = require("xmlbuilder2");
const logger = require("../utils/logger");
const fs = require("fs");

let Client = require("ssh2-sftp-client");

const client = require("../graphql-client/graphql-client").client;
const { sendServerEmail } = require("../email/sendemail");

const AHDineroFormat = "0.00";
const AhDateFormat = "MMDDYYYY";
const NON_ASCII_REGEX = /[^\x20-\x7E]/g;

const repairOpCodes = ["OP4", "OP9", "OP10"];
const replaceOpCodes = ["OP2", "OP5", "OP11", "OP12"];

const ftpSetup = {
  host: process.env.AUTOHOUSE_HOST,
  port: process.env.AUTOHOUSE_PORT,
  username: process.env.AUTOHOUSE_USER,
  password: process.env.AUTOHOUSE_PASSWORD,
  debug:
    process.env.NODE_ENV !== "production"
      ? (message, ...data) => logger.log(message, "DEBUG", "api", null, data)
      : () => {},
  algorithms: {
    serverHostKey: ["ssh-rsa", "ssh-dss", "rsa-sha2-256", "rsa-sha2-512", "ecdsa-sha2-nistp256", "ecdsa-sha2-nistp384"]
  }
};

exports.default = async (req, res) => {
  // Only process if in production environment.
  if (process.env.NODE_ENV !== "production") {
    return res.sendStatus(403);
  }
  // Only process if the appropriate token is provided.
  if (req.headers["x-imex-auth"] !== process.env.AUTOHOUSE_AUTH_TOKEN) {
    return res.sendStatus(401);
  }

  // Send immediate response and continue processing.
  res.status(202).json({
    success: true,
    message: "Processing request ...",
    timestamp: new Date().toISOString()
  });

  try {
    logger.log("autohouse-start", "DEBUG", "api", null, null);
    const allXMLResults = [];
    const allErrors = [];

    const { bodyshops } = await client.request(queries.GET_AUTOHOUSE_SHOPS); //Query for the List of Bodyshop Clients.
    const specificShopIds = req.body.bodyshopIds; // ['uuid];
    const { start, end, skipUpload } = req.body; //YYYY-MM-DD

    const shopsToProcess =
      specificShopIds?.length > 0 ? bodyshops.filter((shop) => specificShopIds.includes(shop.id)) : bodyshops;
    logger.log("autohouse-shopsToProcess-generated", "DEBUG", "api", null, null);

    if (shopsToProcess.length === 0) {
      logger.log("autohouse-shopsToProcess-empty", "DEBUG", "api", null, null);
      return;
    }

    await processShopData(shopsToProcess, start, end, skipUpload, allXMLResults, allErrors);

    await sendServerEmail({
      subject: `Autohouse Report ${moment().format("MM-DD-YY")}`,
      text: `Errors:\n${JSON.stringify(allErrors, null, 2)}\n\nUploaded:\n${JSON.stringify(
        allXMLResults.map((x) => ({
          imexshopid: x.imexshopid,
          filename: x.filename,
          count: x.count,
          result: x.result
        })),
        null,
        2
      )}`
    });

    logger.log("autohouse-end", "DEBUG", "api", null, null);
  } catch (error) {
    logger.log("autohouse-error", "ERROR", "api", null, { error: error.message, stack: error.stack });
  }
};

async function processShopData(shopsToProcess, start, end, skipUpload, allXMLResults, allErrors) {
  for (const bodyshop of shopsToProcess) {
    const erroredJobs = [];
    try {
      logger.log("autohouse-start-shop-extract", "DEBUG", "api", bodyshop.id, {
        shopname: bodyshop.shopname
      });

      const { jobs, bodyshops_by_pk } = await client.request(queries.AUTOHOUSE_QUERY, {
        bodyshopid: bodyshop.id,
        start: start ? moment(start).startOf("day") : moment().subtract(5, "days").startOf("day"),
        ...(end && { end: moment(end).endOf("day") })
      });

      const autoHouseObject = {
        AutoHouseExport: {
          RepairOrder: jobs.map((j) =>
            CreateRepairOrderTag({ ...j, bodyshop: bodyshops_by_pk }, function ({ job, error }) {
              erroredJobs.push({ job: job, error: error.toString() });
            })
          )
        }
      };

      if (erroredJobs.length > 0) {
        logger.log("autohouse-failed-jobs", "ERROR", "api", bodyshop.id, {
          count: erroredJobs.length,
          jobs: JSON.stringify(erroredJobs.map((j) => j.job.ro_number))
        });
      }

      const xmlObj = {
        bodyshopid: bodyshop.id,
        imexshopid: bodyshop.imexshopid,
        xml: builder.create({}, autoHouseObject).end({ allowEmptyTags: true }),
        filename: `IM_${bodyshop.autohouseid}_${moment().format("DDMMYYYY_HHMMss")}.xml`,
        count: autoHouseObject.AutoHouseExport.RepairOrder.length
      };

      if (skipUpload) {
        fs.writeFileSync(`./logs/${xmlObj.filename}`, xmlObj.xml);
      } else {
        await uploadViaSFTP(xmlObj);
      }

      allXMLResults.push({
        bodyshopid: bodyshop.id,
        imexshopid: bodyshop.imexshopid,
        autohouseid: bodyshop.autohouseid,
        count: xmlObj.count,
        filename: xmlObj.filename,
        result: xmlObj.result
      });

      logger.log("autohouse-end-shop-extract", "DEBUG", "api", bodyshop.id, {
        shopname: bodyshop.shopname
      });
    } catch (error) {
      //Error at the shop level.
      logger.log("autohouse-error-shop", "ERROR", "api", bodyshop.id, { error: error.message, stack: error.stack });

      allErrors.push({
        bodyshopid: bodyshop.id,
        imexshopid: bodyshop.imexshopid,
        autohouseid: bodyshop.autohouseid,
        fatal: true,
        errors: [error.toString()]
      });
    } finally {
      allErrors.push({
        bodyshopid: bodyshop.id,
        imexshopid: bodyshop.imexshopid,
        autohouseid: bodyshop.autohouseid,
        errors: erroredJobs.map((ej) => ({
          ro_number: ej.job?.ro_number,
          jobid: ej.job?.id,
          error: ej.error
        }))
      });
    }
  }
}

async function uploadViaSFTP(xmlObj) {
  const sftp = new Client();
  sftp.on("error", (errors) =>
    logger.log("autohouse-sftp-connection-error", "ERROR", "api", xmlObj.bodyshopid, {
      error: errors.message,
      stack: errors.stack
    })
  );
  try {
    //Connect to the FTP and upload all.
    await sftp.connect(ftpSetup);

    try {
      xmlObj.result = await sftp.put(Buffer.from(xmlObj.xml), `${xmlObj.filename}`);
      logger.log("autohouse-sftp-upload", "DEBUG", "api", xmlObj.bodyshopid, {
        imexshopid: xmlObj.imexshopid,
        filename: xmlObj.filename,
        result: xmlObj.result
      });
    } catch (error) {
      logger.log("autohouse-sftp-upload-error", "ERROR", "api", xmlObj.bodyshopid, {
        filename: xmlObj.filename,
        error: error.message,
        stack: error.stack
      });
      throw error;
    }
  } catch (error) {
    logger.log("autohouse-sftp-error", "ERROR", "api", xmlObj.bodyshopid, { error: error.message, stack: error.stack });
    throw error;
  } finally {
    sftp.end();
  }
}

const CreateRepairOrderTag = (job, errorCallback) => {
  //Level 2

  if (!job.job_totals) {
    errorCallback({
      jobid: job.id,
      job: job,
      ro_number: job.ro_number,
      error: { toString: () => "No job totals for RO." }
    });
    return {};
  }

  const repairCosts = CreateCosts(job);

  const LaborDetailLines = generateLaborLines(job.timetickets);

  //Calculate detail only lines.
  const detailAdjustments = job.joblines
    .filter((jl) => jl.ah_detail_line && jl.mod_lbr_ty)
    .reduce(
      (acc, val) => {
        return {
          hours: acc.hours + val.mod_lb_hrs,
          amount: acc.amount.add(
            Dinero({
              amount: Math.round((job.job_totals.rates[val.mod_lbr_ty.toLowerCase()].rate || 0) * val.mod_lb_hrs * 100)
            })
          )
        };
      },
      { hours: 0, amount: Dinero() }
    );

  try {
    const ret = {
      RepairOrderInformation: {
        ShopInternalName: job.bodyshop.autohouseid,
        ID: parseInt(job.ro_number.match(/\d/g).join(""), 10),
        RO: job.ro_number,
        Est: parseInt(job.ro_number.match(/\d/g).join(""), 10), //We no longer use estimate id.
        GUID: job.id,
        TransType: StatusMapping(job.status, job.bodyshop.md_ro_statuses),
        ShopName: job.bodyshop.shopname,
        ShopAddress: job.bodyshop.address1,
        ShopCity: job.bodyshop.city,
        ShopState: job.bodyshop.state,
        ShopZip: job.bodyshop.zip_post,
        ShopPhone: job.bodyshop.phone,
        EstimatorID: `${job.est_ct_ln ? job.est_ct_ln : ""}${
          job.est_ct_ln ? ", " : ""
        }${job.est_ct_fn ? job.est_ct_fn : ""}`,
        EstimatorName: `${job.est_ct_ln ? job.est_ct_ln : ""}${
          job.est_ct_ln ? ", " : ""
        }${job.est_ct_fn ? job.est_ct_fn : ""}`
      },
      CustomerInformation: {
        FirstName: "",
        LastName: "",
        Street: "",
        City: "",
        State: "",
        Zip: (job.ownr_zip && job.ownr_zip.substring(0, 3)) || "",
        Phone1: "",
        Phone2: null,
        Phone2Extension: null,
        Phone3: null,
        Phone3Extension: null,
        FileComments: null,
        Source: null,
        Email: "",
        RetWhsl: null,
        Cat: null,
        InsuredorClaimantFlag: null
      },
      VehicleInformation: {
        Year: job.v_model_yr
          ? parseInt(job.v_model_yr.match(/\d/g))
            ? parseInt(job.v_model_yr.match(/\d/g).join(""), 10)
            : ""
          : "",
        Make: job.v_make_desc || "",
        Model: job.v_model_desc || "",
        VIN: job.v_vin || "",
        License: job.plate_no,
        MileageIn: job.kmin || 0,
        Vehiclecolor: job.v_color,
        VehicleProductionDate: null,
        VehiclePaintCode: null,
        VehicleTrimCode: null,
        VehicleBodyStyle: null,
        DriveableFlag: job.driveable ? "Y" : "N"
      },

      InsuranceInformation: {
        InsuranceCo: job.ins_co_nm || "",
        CompanyName: job.ins_co_nm || "",
        Address: job.ins_addr1 || "",
        City: job.ins_city || "",
        State: job.ins_st || "",
        Zip: job.ins_zip || "",
        Phone: job.ins_ph1 || "",
        Fax: job.ins_fax || "",
        ClaimType: null,
        LossType: job.loss_type || "",
        Policy: job.policy_no || "",
        Claim: job.clm_no || "",
        InsuredLastName: null,
        InsuredFirstName: null,
        ClaimantLastName: null,
        ClaimantFirstName: null,
        Assignment: null,
        InsuranceAgentLastName: null,
        InsuranceAgentFirstName: null,
        InsAgentPhone: null,
        InsideAdjuster: null,
        OutsideAdjuster: null
      },
      Dates: {
        DateofLoss: (job.loss_date && moment(job.loss_date).format(AhDateFormat)) || "",
        InitialCustomerContactDate: null,
        FirstFollowUpDate: null,
        ReferralDate: null,
        EstimateAppointmentDate: null,
        SecondFollowUpDate: null,
        AssignedDate: (job.asgn_date && moment(job.asgn_date).format(AhDateFormat)) || "",
        EstComplete: null,
        CustomerAuthorizationDate: null,
        InsuranceAuthorizationDate: null,
        DateOpened:
          (job.date_open && moment(job.date_open).tz(job.bodyshop.timezone).format(AhDateFormat)) ||
          (job.created_at && moment(job.created_at).tz(job.bodyshop.timezone).format(AhDateFormat)) ||
          "",
        ScheduledArrivalDate:
          (job.scheduled_in && moment(job.scheduled_in).tz(job.bodyshop.timezone).format(AhDateFormat)) || "",
        CarinShop: (job.actual_in && moment(job.actual_in).tz(job.bodyshop.timezone).format(AhDateFormat)) || "",
        InsInspDate: null,
        StartDate: job.date_repairstarted
          ? (job.date_repairstarted && moment(job.date_repairstarted).tz(job.bodyshop.timezone).format(AhDateFormat)) ||
            ""
          : (job.date_repairstarted && moment(job.actual_in).tz(job.bodyshop.timezone).format(AhDateFormat)) || "",
        PartsOrder: null,
        TeardownHold: null,
        SupplementSubmittedDate: null,
        SupplementApprovedDate: null,
        AssntoBody: null,
        AssntoMech: null,
        AssntoPaint: null,
        AssntoDetail: null,
        // PromiseDate:
        //   (job.scheduled_completion &&
        //     moment(job.scheduled_completion).format(AhDateFormat)) ||
        //   "",
        //InsuranceTargetOut: null,
        CarComplete:
          (job.actual_completion && moment(job.actual_completion).tz(job.bodyshop.timezone).format(AhDateFormat)) || "",
        DeliveryAppointmentDate:
          // (job.scheduled_delivery &&
          //   moment(job.scheduled_delivery)
          //     .tz(job.bodyshop.timezone)
          //     .format(AhDateFormat)) ||
          (job.scheduled_completion &&
            moment(job.scheduled_completion).tz(job.bodyshop.timezone).format(AhDateFormat)) ||
          "",
        DateClosed:
          (job.date_invoiced && moment(job.date_invoiced).tz(job.bodyshop.timezone).format(AhDateFormat)) || "",
        CustomerPaidInFullDate: null,
        InsurancePaidInFullDate: null,
        CustPickup:
          (job.actual_delivery && moment(job.actual_delivery).tz(job.bodyshop.timezone).format(AhDateFormat)) || "",
        AccountPostedDate:
          job.date_exported && moment(job.date_exported).tz(job.bodyshop.timezone).format(AhDateFormat),
        CSIProcessedDate: null,
        ThankYouLetterSent: null,
        AdditionalFollowUpDate: null
      },
      Rates: {
        BodyRate: job.rate_lab || 0,
        RefinishRate: job.rate_lar || 0,
        MechanicalRate: job.rate_lam || 0,
        StructuralRate: job.rate_las || 0,
        ElectricalRate: job.rate_lae || 0,
        FrameRate: job.rate_laf || 0,
        GlassRate: job.rate_lag || 0,
        DetailRate: 0, // job.rate_lad || 0,
        LaborMiscRate: 0,
        PMRate: job.rate_mapa || 0,
        BMRate: job.rate_mash || 0,
        TaxRate: (job.parts_tax_rates && job.parts_tax_rates.PAN && job.parts_tax_rates.PAN.prt_tax_rt) || 0,
        StorageRateperDay: 0,
        DaysStored: 0
      },
      // EstimateTotals: {
      //   BodyHours: null,
      //   RefinishHours: null,
      //   MechanicalHours: null,
      //   StructuralHours: null,
      //   PartsTotal: null,
      //   PartsOEM: null,
      //   PartsAM: null,
      //   PartsReconditioned: null,
      //   PartsRecycled: null,
      //   PartsOther: null,
      //   SubletTotal: null,
      //   BodyLaborTotal: null,
      //   RefinishLaborTotal: null,
      //   MechanicalLaborTotal: null,
      //   StructuralLaborTotal: null,
      //   MiscellaneousChargeTotal: null,
      //   PMTotal: null,
      //   BMTotal: null,
      //   MiscTotal: null,
      //   TowingTotal: null,
      //   StorageTotal: null,
      //   DetailTotal: null,
      //   SalesTaxTotal: null,
      //   GrossTotal: null,
      //   DeductibleTotal: null,
      //   DepreciationTotal: null,
      //   Discount: null,
      //   CustomerPay: null,
      //   InsurancePay: null,
      //   Deposit: null,
      //   AmountDue: null,
      // },
      // SupplementTotals: {
      //   BodyHours: null,
      //   RefinishHours: null,
      //   MechanicalHours: null,
      //   StructuralHours: null,
      //   PartsTotal: null,
      //   PartsOEM: null,
      //   PartsAM: null,
      //   PartsReconditioned: null,
      //   PartsRecycled: null,
      //   PartsOther: null,
      //   SubletTotal: null,
      //   BodyLaborTotal: null,
      //   RefinishLaborTotal: null,
      //   MechanicalLaborTotal: null,
      //   StructuralLaborTotal: null,
      //   MiscellaneousChargeTotal: null,
      //   PMTotal: null,
      //   BMTotal: null,
      //   MiscTotal: null,
      //   TowingTotal: null,
      //   StorageTotal: null,
      //   DetailTotal: null,
      //   SalesTaxTotal: null,
      //   GrossTotal: null,
      //   DeductibleTotal: null,
      //   DepreciationTotal: null,
      //   Discount: null,
      //   CustomerPay: null,
      //   InsurancePay: null,
      //   Deposit: null,
      //   AmountDue: null,
      // },
      RevisedTotals: {
        BodyHours: job.job_totals.rates.lab.hours.toFixed(2),
        BodyRepairHours: job.joblines
          .filter((line) => repairOpCodes.includes(line.lbr_op))
          .reduce((acc, val) => acc + val.mod_lb_hrs, 0)
          .toFixed(2),
        BodyReplaceHours: job.joblines
          .filter((line) => replaceOpCodes.includes(line.lbr_op))
          .reduce((acc, val) => acc + val.mod_lb_hrs, 0)
          .toFixed(2),
        RefinishHours: job.job_totals.rates.lar.hours.toFixed(2),
        MechanicalHours: job.job_totals.rates.lam.hours.toFixed(2),
        StructuralHours: job.job_totals.rates.las.hours.toFixed(2),

        ElectricalHours: job.job_totals.rates.lae.hours.toFixed(2),
        FrameHours: job.job_totals.rates.laf.hours.toFixed(2),
        GlassHours: job.job_totals.rates.lag.hours.toFixed(2),
        DetailHours: detailAdjustments.hours, //job.job_totals.rates.lad.hours.toFixed(2),
        LaborMiscHours: (
          job.job_totals.rates.la1.hours +
          job.job_totals.rates.la2.hours +
          job.job_totals.rates.la3.hours +
          job.job_totals.rates.la4.hours +
          job.job_totals.rates.lau.hours -
          detailAdjustments.hours
        ).toFixed(2),

        PartsTotal: Dinero(job.job_totals.parts.parts.total).toFormat(AHDineroFormat),
        PartsTotalCost: repairCosts.PartsTotalCost.toFormat(AHDineroFormat),
        PartsOEM: Dinero(job.job_totals.parts.parts.list.PAN && job.job_totals.parts.parts.list.PAN.total)
          .add(Dinero(job.job_totals.parts.parts.list.PAP && job.job_totals.parts.parts.list.PAP.total))
          .toFormat(AHDineroFormat),
        PartsOEMCost: repairCosts.PartsOemCost.toFormat(AHDineroFormat),
        PartsAM: Dinero(job.job_totals.parts.parts.list.PAA && job.job_totals.parts.parts.list.PAA.total).toFormat(
          AHDineroFormat
        ),
        PartsAMCost: repairCosts.PartsAMCost.toFormat(AHDineroFormat),
        PartsReconditioned: repairCosts.PartsReconditionedCost.toFormat(AHDineroFormat),
        PartsReconditionedCost: repairCosts.PartsReconditionedCost.toFormat(AHDineroFormat),
        PartsRecycled: Dinero(
          job.job_totals.parts.parts.list.PAL && job.job_totals.parts.parts.list.PAL.total
        ).toFormat(AHDineroFormat),
        PartsRecycledCost: repairCosts.PartsRecycledCost.toFormat(AHDineroFormat),
        PartsOther: Dinero(job.job_totals.parts.parts.list.PAO && job.job_totals.parts.parts.list.PAO.total).toFormat(
          AHDineroFormat
        ),
        PartsOtherCost: repairCosts.PartsOtherCost.toFormat(AHDineroFormat),
        SubletTotal: Dinero(job.job_totals.parts.sublets.total).toFormat(AHDineroFormat),
        SubletTotalCost: repairCosts.SubletTotalCost.toFormat(AHDineroFormat),
        BodyLaborTotal: Dinero(job.job_totals.rates.lab.total).toFormat(AHDineroFormat),
        BodyLaborTotalCost: repairCosts.BodyLaborTotalCost.toFormat(AHDineroFormat),
        RefinishLaborTotal: Dinero(job.job_totals.rates.lar.total).toFormat(AHDineroFormat),
        RefinishLaborTotalCost: repairCosts.RefinishLaborTotalCost.toFormat(AHDineroFormat),
        MechanicalLaborTotal: Dinero(job.job_totals.rates.lam.total).toFormat(AHDineroFormat),
        MechanicalLaborTotalCost: repairCosts.MechanicalLaborTotalCost.toFormat(AHDineroFormat),
        StructuralLaborTotal: Dinero(job.job_totals.rates.las.total).toFormat(AHDineroFormat),
        StructuralLaborTotalCost: repairCosts.StructuralLaborTotalCost.toFormat(AHDineroFormat),

        ElectricalLaborTotal: Dinero(job.job_totals.rates.lae.total).toFormat(AHDineroFormat),
        ElectricalLaborTotalCost: repairCosts.ElectricalLaborTotalCost.toFormat(AHDineroFormat),
        FrameLaborTotal: Dinero(job.job_totals.rates.laf.total).toFormat(AHDineroFormat),
        FrameLaborTotalCost: repairCosts.FrameLaborTotalCost.toFormat(AHDineroFormat),
        GlassLaborTotal: Dinero(job.job_totals.rates.lag.total).toFormat(AHDineroFormat),
        GlassLaborTotalCost: repairCosts.GlassLaborTotalCost.toFormat(AHDineroFormat),
        DetailLaborTotal: detailAdjustments.amount.toFormat(AHDineroFormat),
        //  Dinero(job.job_totals.rates.lad.total).toFormat(
        //   AHDineroFormat
        // ),
        DetailLaborTotalCost: Dinero().toFormat(AHDineroFormat),
        // repairCosts.DetailLaborTotalCost.toFormat(AHDineroFormat),
        LaborMiscTotal: Dinero(job.job_totals.rates.la1.total)
          .add(Dinero(job.job_totals.rates.la2.total))
          .add(Dinero(job.job_totals.rates.la3.total))
          .add(Dinero(job.job_totals.rates.la4.total))
          .add(Dinero(job.job_totals.rates.lau.total))
          .subtract(detailAdjustments.amount)
          .toFormat(AHDineroFormat),
        LaborMiscTotalCost: 0,
        MiscellaneousChargeTotal: 0,
        MiscellaneousChargeTotalCost: 0,
        PMTotal: Dinero(job.job_totals.rates.mapa.total).toFormat(AHDineroFormat),
        PMTotalCost: repairCosts.PMTotalCost.toFormat(AHDineroFormat),
        BMTotal: Dinero(job.job_totals.rates.mash.total).toFormat(AHDineroFormat),
        BMTotalCost: repairCosts.BMTotalCost.toFormat(AHDineroFormat),
        MiscTotal: Dinero(job.job_totals.additional.additionalCosts).toFormat(AHDineroFormat),
        MiscTotalCost: 0,
        TowingTotal: Dinero(job.job_totals.additional.towing).toFormat(AHDineroFormat),
        TowingTotalCost: repairCosts.TowingTotalCost.toFormat(AHDineroFormat),
        StorageTotal: Dinero(job.job_totals.additional.storage).toFormat(AHDineroFormat),
        StorageTotalCost: repairCosts.StorageTotalCost.toFormat(AHDineroFormat),
        DetailTotal: detailAdjustments.amount.toFormat(AHDineroFormat),
        DetailTotalCost: 0,
        SalesTaxTotal: Dinero(job.job_totals.totals.local_tax)
          .add(Dinero(job.job_totals.totals.state_tax))
          .add(Dinero(job.job_totals.totals.federal_tax))
          .add(Dinero(job.job_totals.additional.pvrt))
          .toFormat(AHDineroFormat),
        SalesTaxTotalCost: 0,
        GrossTotal: Dinero(job.job_totals.totals.total_repairs).toFormat(AHDineroFormat),
        DeductibleTotal: Dinero({
          amount: Math.round((job.ded_amt || 0) * 100)
        }).toFormat(AHDineroFormat),
        DepreciationTotal: Dinero(job.job_totals.totals.custPayable.dep_taxes).toFormat(AHDineroFormat),
        Discount: Dinero(job.job_totals.additional.adjustments).toFormat(AHDineroFormat),
        CustomerPay: Dinero(job.job_totals.totals.custPayable.total).toFormat(AHDineroFormat),
        InsurancePay: Dinero(job.job_totals.totals.total_repairs)
          .subtract(Dinero(job.job_totals.totals.custPayable.total))
          .toFormat(AHDineroFormat),
        Deposit: 0,
        AmountDue: 0
      },
      Misc: {
        ProductionStatus: null,
        StatusDescription: null,
        Hub50Comment: null,
        DateofChange: null,
        BodyTechName: null,
        TotalLossYN: job.tlos_ind ? "Y" : "N",
        InsScreenCommentsLine1: null,
        InsScreenCommentsLine2: null,
        AssignmentCaller: null,
        AssignmentDivision: null,
        LocationofPrimaryImpact: (job.area_of_damage && job.area_of_damage.impact1) || 0,
        LocationofSecondaryImpact: (job.area_of_damage && job.area_of_damage.impact2) || 0,
        PaintTechID: null,
        PaintTechName: null,
        ImportType: null,
        ImportFile: null,
        GSTTax: Dinero(job.job_totals.totals.federal_tax).toFormat(AHDineroFormat),
        RepairDelayStatusCode: null,
        RepairDelaycomment: null,
        AgentMktgID: null,
        AgentCity: null,
        Picture1: null,
        Picture2: null,
        ExtNoteDate: null,
        RentalOrdDate: null,
        RentalPUDate: null,
        RentalDueDate: null,
        RentalActRetDate: null,
        RentalCompanyID: null,
        // CSIID: null,
        InsGroupCode: null
      },
      DetailLines: {
        DetailLine:
          job.joblines.length > 0
            ? job.joblines.map((jl) => GenerateDetailLines(job, jl, job.bodyshop.md_order_statuses))
            : [generateNullDetailLine()]
      },
      LaborDetailLines: {
        LaborDetailLine: LaborDetailLines
      }
    };
    return ret;
  } catch (error) {
    logger.log("autohouse-job-calculate-error", "ERROR", "api", null, { error: error.message, stack: error.stack });
    errorCallback({ jobid: job.id, ro_number: job.ro_number, error });
  }
};

const CreateCosts = (job) => {
  //Create a mapping based on AH Requirements

  //For DMS, the keys in the object below are the CIECA part types.
  const billTotalsByCostCenters = job.bills.reduce((bill_acc, bill_val) => {
    //At the bill level.
    bill_val.billlines.map((line_val) => {
      //At the bill line level.

      if (!bill_acc[line_val.cost_center]) bill_acc[line_val.cost_center] = Dinero();

      bill_acc[line_val.cost_center] = bill_acc[line_val.cost_center].add(
        Dinero({
          amount: Math.round((line_val.actual_cost || 0) * 100)
        })
          .multiply(line_val.quantity)
          .multiply(bill_val.is_credit_memo ? -1 : 1)
      );

      return null;
    });
    return bill_acc;
  }, {});

  //If the hourly rates for job costing are set, add them in.
  if (
    job.bodyshop.jc_hourly_rates &&
    (job.bodyshop.jc_hourly_rates.mapa ||
      typeof job.bodyshop.jc_hourly_rates.mapa === "number" ||
      isNaN(job.bodyshop.jc_hourly_rates.mapa) === false)
  ) {
    if (!billTotalsByCostCenters[job.bodyshop.md_responsibility_centers.defaults.costs.MAPA])
      billTotalsByCostCenters[job.bodyshop.md_responsibility_centers.defaults.costs.MAPA] = Dinero();
    if (job.bodyshop.use_paint_scale_data === true) {
      if (job.mixdata.length > 0) {
        billTotalsByCostCenters[job.bodyshop.md_responsibility_centers.defaults.costs.MAPA] = Dinero({
          amount: Math.round(((job.mixdata[0] && job.mixdata[0].totalliquidcost) || 0) * 100)
        });
      } else {
        billTotalsByCostCenters[job.bodyshop.md_responsibility_centers.defaults.costs.MAPA] = billTotalsByCostCenters[
          job.bodyshop.md_responsibility_centers.defaults.costs.MAPA
        ].add(
          Dinero({
            amount: Math.round((job.bodyshop.jc_hourly_rates && job.bodyshop.jc_hourly_rates.mapa * 100) || 0)
          }).multiply(job.job_totals.rates.mapa.hours)
        );
      }
    } else {
      billTotalsByCostCenters[job.bodyshop.md_responsibility_centers.defaults.costs.MAPA] = billTotalsByCostCenters[
        job.bodyshop.md_responsibility_centers.defaults.costs.MAPA
      ].add(
        Dinero({
          amount: Math.round((job.bodyshop.jc_hourly_rates && job.bodyshop.jc_hourly_rates.mapa * 100) || 0)
        }).multiply(job.job_totals.rates.mapa.hours)
      );
    }
  }
  if (job.bodyshop.jc_hourly_rates && job.bodyshop.jc_hourly_rates.mash) {
    if (!billTotalsByCostCenters[job.bodyshop.md_responsibility_centers.defaults.costs.MASH])
      billTotalsByCostCenters[job.bodyshop.md_responsibility_centers.defaults.costs.MASH] = Dinero();
    billTotalsByCostCenters[job.bodyshop.md_responsibility_centers.defaults.costs.MASH] = billTotalsByCostCenters[
      job.bodyshop.md_responsibility_centers.defaults.costs.MASH
    ].add(
      Dinero({
        amount: Math.round((job.bodyshop.jc_hourly_rates && job.bodyshop.jc_hourly_rates.mash * 100) || 0)
      }).multiply(job.job_totals.rates.mash.hours)
    );
  }
  //Uses CIECA Labor types.
  const ticketTotalsByCostCenter = job.timetickets.reduce((ticket_acc, ticket_val) => {
    //At the invoice level.
    if (!ticket_acc[ticket_val.cost_center]) ticket_acc[ticket_val.cost_center] = Dinero();

    ticket_acc[ticket_val.cost_center] = ticket_acc[ticket_val.cost_center].add(
      Dinero({
        amount: Math.round((ticket_val.rate || 0) * 100)
      }).multiply((ticket_val.flat_rate ? ticket_val.productivehrs : ticket_val.actualhrs) || 0)
    );

    return ticket_acc;
  }, {});
  //CIECA STANDARD MAPPING OBJECT.

  const ciecaObj = {
    ATS: "ATS",
    LA1: "LA1",
    LA2: "LA2",
    LA3: "LA3",
    LA4: "LA4",
    LAA: "LAA",
    LAB: "LAB",
    LAD: "LAD",
    LAE: "LAE",
    LAF: "LAF",
    LAG: "LAG",
    LAM: "LAM",
    LAR: "LAR",
    LAS: "LAS",
    LAU: "LAU",
    PAA: "PAA",
    PAC: "PAC",
    PAG: "PAG",
    PAL: "PAL",
    PAM: "PAM",
    PAN: "PAN",
    PAO: "PAO",
    PAP: "PAP",
    PAR: "PAR",
    PAS: "PAS",
    TOW: "TOW",
    MAPA: "MAPA",
    MASH: "MASH",
    PASL: "PASL"
  };
  const defaultCosts =
    job.bodyshop.cdk_dealerid || job.bodyshop.pbs_serialnumber || job.bodyshop.rr_dealerid
      ? ciecaObj
      : job.bodyshop.md_responsibility_centers.defaults.costs;

  return {
    PartsTotalCost: Object.keys(billTotalsByCostCenters).reduce((acc, key) => {
      if (
        key !== defaultCosts.PAS &&
        key !== defaultCosts.PASL &&
        key !== defaultCosts.MAPA &&
        key !== defaultCosts.MASH &&
        key !== defaultCosts.TOW
      )
        return acc.add(billTotalsByCostCenters[key]);
      return acc;
    }, Dinero()),
    PartsOemCost: (billTotalsByCostCenters[defaultCosts.PAN] || Dinero()).add(
      billTotalsByCostCenters[defaultCosts.PAP] || Dinero()
    ),
    PartsAMCost: billTotalsByCostCenters[defaultCosts.PAA] || Dinero(),
    PartsReconditionedCost: billTotalsByCostCenters[defaultCosts.PAM] || Dinero(),
    PartsRecycledCost: billTotalsByCostCenters[defaultCosts.PAL] || Dinero(),
    PartsOtherCost: billTotalsByCostCenters[defaultCosts.PAO] || Dinero(),
    SubletTotalCost:
      billTotalsByCostCenters[defaultCosts.PAS] || Dinero(billTotalsByCostCenters[defaultCosts.PASL] || Dinero()),
    BodyLaborTotalCost: ticketTotalsByCostCenter[defaultCosts.LAB] || Dinero(),
    RefinishLaborTotalCost: ticketTotalsByCostCenter[defaultCosts.LAR] || Dinero(),
    MechanicalLaborTotalCost: ticketTotalsByCostCenter[defaultCosts.LAM] || Dinero(),
    StructuralLaborTotalCost: ticketTotalsByCostCenter[defaultCosts.LAS] || Dinero(),
    ElectricalLaborTotalCost: ticketTotalsByCostCenter[defaultCosts.LAE] || Dinero(),
    FrameLaborTotalCost: ticketTotalsByCostCenter[defaultCosts.LAF] || Dinero(),
    GlassLaborTotalCost: ticketTotalsByCostCenter[defaultCosts.LAG] || Dinero(),
    DetailLaborTotalCost: Dinero(),
    //   ticketTotalsByCostCenter[defaultCosts.LAD] || Dinero(),
    LaborMiscTotalCost: (ticketTotalsByCostCenter[defaultCosts.LA1] || Dinero())
      .add(ticketTotalsByCostCenter[defaultCosts.LA2] || Dinero())
      .add(ticketTotalsByCostCenter[defaultCosts.LA2] || Dinero())
      .add(ticketTotalsByCostCenter[defaultCosts.LA3] || Dinero())
      .add(ticketTotalsByCostCenter[defaultCosts.LA4] || Dinero())
      .add(ticketTotalsByCostCenter[defaultCosts.LAU] || Dinero()),
    PMTotalCost: billTotalsByCostCenters[defaultCosts.MAPA] || Dinero(),
    BMTotalCost: billTotalsByCostCenters[defaultCosts.MASH] || Dinero(),
    MiscTotalCost: billTotalsByCostCenters[defaultCosts.PAO] || Dinero(),
    TowingTotalCost: billTotalsByCostCenters[defaultCosts.TOW] || Dinero(),
    StorageTotalCost: Dinero(),
    DetailTotal: Dinero(),
    DetailTotalCost: Dinero(),
    SalesTaxTotalCost: Dinero()
  };
};

const generateLaborLines = (timetickets) => {
  if (!timetickets || timetickets.length === 0) return [];

  const codeToProps = {
    LAB: { actual: "LaborBodyActualHours", flag: "LaborBodyFlagHours", cost: "LaborBodyCost" },
    LAM: { actual: "LaborMechanicalActualHours", flag: "LaborMechanicalFlagHours", cost: "LaborMechanicalCost" },
    LAG: { actual: "LaborGlassActualHours", flag: "LaborGlassFlagHours", cost: "LaborGlassCost" },
    LAS: { actual: "LaborStructuralActualHours", flag: "LaborStructuralFlagHours", cost: "LaborStructuralCost" },
    LAE: { actual: "LaborElectricalActualHours", flag: "LaborElectricalFlagHours", cost: "LaborElectricalCost" },
    LAA: { actual: "LaborAluminumActualHours", flag: "LaborAluminumFlagHours", cost: "LaborAluminumCost" },
    LAR: { actual: "LaborRefinishActualHours", flag: "LaborRefinishFlagHours", cost: "LaborRefinishCost" },
    LAU: { actual: "LaborDetailActualHours", flag: "LaborDetailFlagHours", cost: "LaborDetailCost" },
    LA1: { actual: "LaborOtherActualHours", flag: "LaborOtherFlagHours", cost: "LaborOtherCost" },
    LA2: { actual: "LaborOtherActualHours", flag: "LaborOtherFlagHours", cost: "LaborOtherCost" },
    LA3: { actual: "LaborOtherActualHours", flag: "LaborOtherFlagHours", cost: "LaborOtherCost" },
    LA4: { actual: "LaborOtherActualHours", flag: "LaborOtherFlagHours", cost: "LaborOtherCost" }
  };

  return timetickets.map((ticket, idx) => {
    const { ciecacode, employee, actualhrs = 0, productivehrs = 0, rate = 0 } = ticket;
    const isFlatRate = employee?.flat_rate;
    const hours = isFlatRate ? productivehrs : actualhrs;
    const cost = rate * hours;

    const laborDetail = {
      LaborDetailLineNumber: idx + 1,
      TechnicianNameFirst: employee?.first_name || "",
      TechnicianNameLast: employee?.last_name || "",
      LaborBodyActualHours: 0,
      LaborMechanicalActualHours: 0,
      LaborGlassActualHours: 0,
      LaborStructuralActualHours: 0,
      LaborElectricalActualHours: 0,
      LaborAluminumActualHours: 0,
      LaborRefinishActualHours: 0,
      LaborDetailActualHours: 0,
      LaborOtherActualHours: 0,
      LaborBodyFlagHours: 0,
      LaborMechanicalFlagHours: 0,
      LaborGlassFlagHours: 0,
      LaborStructuralFlagHours: 0,
      LaborElectricalFlagHours: 0,
      LaborAluminumFlagHours: 0,
      LaborRefinishFlagHours: 0,
      LaborDetailFlagHours: 0,
      LaborOtherFlagHours: 0,
      LaborBodyCost: 0,
      LaborMechanicalCost: 0,
      LaborGlassCost: 0,
      LaborStructuralCost: 0,
      LaborElectricalCost: 0,
      LaborAluminumCost: 0,
      LaborRefinishCost: 0,
      LaborDetailCost: 0,
      LaborOtherCost: 0
    };

    const effectiveCiecacode = ciecacode || "LA4";

    if (codeToProps[effectiveCiecacode]) {
      const { actual, flag, cost: costProp } = codeToProps[effectiveCiecacode];
      laborDetail[actual] = actualhrs;
      laborDetail[flag] = productivehrs;
      laborDetail[costProp] = cost;
    }

    return laborDetail;
  });
};

const StatusMapping = (status, md_ro_statuses) => {
  //Possible return statuses EST, SCH, ARR, IPR, RDY, DEL, CLO, CAN, UNDEFINED.
  const {
    default_imported,
    default_open,
    default_scheduled,
    default_arrived,
    default_completed,
    default_delivered,
    default_invoiced,
    default_exported,
    default_void
  } = md_ro_statuses;

  if (status === default_open || status === default_imported) return "EST";
  else if (status === default_scheduled) return "SCH";
  else if (status === default_arrived) return "ARR";
  else if (status === default_completed) return "RDY";
  else if (status === default_delivered) return "DEL";
  else if (status === default_invoiced || status === default_exported) return "CLO";
  else if (status === default_void) return "VOID";
  else if (md_ro_statuses.production_statuses.includes(status)) return "IPR";
  else return "UNDEFINED";
};

const GenerateDetailLines = (job, line, statuses) => {
  const ret = {
    BackOrdered: line.status === statuses.default_bo ? "1" : "0",
    Cost: (line.billlines[0] && (line.billlines[0].actual_cost * line.billlines[0].quantity).toFixed(2)) || 0,
    //Critical: null,
    Description: line.line_desc ? line.line_desc.replace(NON_ASCII_REGEX, "") : "",
    DiscountMarkup: line.prt_dsmk_m || 0,
    InvoiceNumber: line.billlines[0] && line.billlines[0].bill.invoice_number,
    IOUPart: 0,
    LineNumber: line.line_no || 0,
    MarkUp: null,
    OrderedOn:
      (line.parts_order_lines[0] && moment(line.parts_order_lines[0].parts_order.order_date).format(AhDateFormat)) ||
      "",
    OriginalCost: null,
    OriginalInvoiceNumber: null,
    PriceEach: line.act_price || 0,
    PartNumber: line.oem_partno ? line.oem_partno.replace(NON_ASCII_REGEX, "") : "",
    ProfitPercent: null,
    PurchaseOrderNumber: null,
    Qty: line.part_qty || 0,
    Status: line.status || "",
    SupplementNumber: line.line_ind ? line.line_ind.replace(/[^\d.-]/g, "") : 0,
    Type: line.part_type || "",
    Vendor: (line.billlines[0] && line.billlines[0].bill.vendor.name) || "",
    VendorPaid: null,
    VendorPrice: (line.billlines[0] && line.billlines[0].actual_price.toFixed(2)) || 0,
    Deleted: null,
    ExpectedOn: null,
    ReceivedOn: line.billlines[0] && moment(line.billlines[0].bill.date).format(AhDateFormat),
    OrderedBy: null,
    ShipVia: null,
    VendorContact: null,
    EstimateAmount: (line.act_price * line.part_qty).toFixed(2) || 0 //Rebecca
  };
  return ret;
};

const generateNullDetailLine = () => {
  return {
    BackOrdered: "0",
    Cost: 0,
    Critical: null,
    Description: "No Lines on Estimate",
    DiscountMarkup: 0,
    InvoiceNumber: null,
    IOUPart: 0,
    LineNumber: 0,
    MarkUp: null,
    OrderedOn: "",
    OriginalCost: null,
    OriginalInvoiceNumber: null,
    PriceEach: 0,
    PartNumber: 0,
    ProfitPercent: null,
    PurchaseOrderNumber: null,
    Qty: 0,
    Status: "",
    SupplementNumber: 0,
    Type: "",
    Vendor: "",
    VendorPaid: null,
    VendorPrice: 0,
    Deleted: 0,
    ExpectedOn: "",
    ReceivedOn: "",
    OrderedBy: "",
    ShipVia: "",
    VendorContact: "",
    EstimateAmount: 0
  };
};
