// no-dd-sa:javascript-code-style/assignment-name
// Handler for VehicleDamageEstimateChgRq

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

const {
  GET_JOB_BY_ID,
  UPDATE_JOB_BY_ID,
  SOFT_DELETE_JOBLINES_BY_IDS,
  GET_JOBLINES_NOTES_BY_JOBID_UNQSEQ,
  GET_JOBLINE_IDS_BY_JOBID_UNQSEQ,
  UPDATE_JOBLINE_BY_PK,
  INSERT_JOBLINES
} = require("../partsManagement.queries");

/**
 * Finds a job by shop ID and job ID.
 * @param shopId
 * @param jobId
 * @param logger
 * @returns {Promise<*|null>}
 */
const findJob = async (shopId, jobId, logger) => {
  try {
    const { jobs } = await client.request(GET_JOB_BY_ID, { shopid: shopId, jobid: jobId });
    return jobs?.[0] || null;
  } catch (err) {
    logger.log("parts-job-lookup-failed", "error", null, null, { error: err });
    return null;
  }
};

/**
 * Extracts updated job data from the request payload.
 * Mirrors AddRq for parts_tax_rates + driveable when present.
 * @param rq
 */
const extractUpdatedJobData = (rq) => {
  const doc = rq.DocumentInfo || {};
  const claim = rq.ClaimInfo || {};
  const policyNo = claim.PolicyInfo?.PolicyInfo?.PolicyNum || claim.PolicyInfo?.PolicyNum || null;

  const out = {
    comment: doc.Comment || null,
    clm_no: claim.ClaimNum || null,
    // TODO (future): status omitted intentionally to avoid overwriting with 'Auth Cust'
    policy_no: policyNo
  };

  if (rq.ProfileInfo) {
    out.parts_tax_rates = extractPartsTaxRates(rq.ProfileInfo);
  }

  if (rq.VehicleInfo?.Condition?.DrivableInd !== undefined) {
    out.driveable = !!rq.VehicleInfo.Condition.DrivableInd;
  }

  return out;
};

/**
 * Build jobline payloads for updates/inserts (no split between parts & labor).
 * - Refinish labor aggregated into lbr_* secondary fields and lbr_amt.
 * - SUBLET-only -> PAS line with act_price = SubletAmount.
 * - Notes merged with current DB value by unq_seq.
 */
const extractUpdatedJobLines = (addsChgs = {}, jobId, currentJobLineNotes = {}) => {
  const linesIn = Array.isArray(addsChgs.DamageLineInfo) ? addsChgs.DamageLineInfo : [addsChgs.DamageLineInfo || {}];

  const coerceManual = (val) =>
    val === true || val === 1 || val === "1" || (typeof val === "string" && val.toUpperCase() === "Y");

  const out = [];

  for (const line of linesIn) {
    if (!line || Object.keys(line).length === 0) continue;

    const partInfo = line.PartInfo || {};
    const laborInfo = line.LaborInfo || {};
    const refinishInfo = line.RefinishLaborInfo || {};
    const subletInfo = line.SubletInfo || {};

    const base = {
      jobid: jobId,
      line_no: parseInt(line.LineNum || 0, 10),
      unq_seq: parseInt(line.UniqueSequenceNum || 0, 10),
      status: line.LineStatusCode || null,
      line_desc: line.LineDesc || null,
      manual_line: false
      // manual_line: line.ManualLineInd !== undefined ? coerceManual(line.ManualLineInd) : null
    };

    const lineOut = { ...base };

    // --- Notes merge ---
    const unqSeq = lineOut.unq_seq;
    const currentNotes = currentJobLineNotes?.[unqSeq] || null;
    const newNotes = line.LineMemo || null;
    if (newNotes && currentNotes) {
      if (currentNotes === newNotes || currentNotes.includes(newNotes)) lineOut.notes = currentNotes;
      else lineOut.notes = `${currentNotes} | ${newNotes}`;
    } else if (newNotes) lineOut.notes = newNotes;
    else if (currentNotes) lineOut.notes = currentNotes;
    else lineOut.notes = null;
    // --- end notes merge ---

    const hasPart = Object.keys(partInfo).length > 0;
    const hasSublet = Object.keys(subletInfo).length > 0;

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

      lineOut.act_price = parseFloat(partInfo?.PartPrice || 0);
      lineOut.db_price = parseFloat(partInfo?.OEMPartPrice || 0);

      if (partInfo.TaxableInd !== undefined) {
        const t = partInfo.TaxableInd;
        lineOut.tax_part = t === true || t === 1 || t === "1" || (typeof t === "string" && t.toUpperCase() === "Y");
      }
    } else if (hasSublet) {
      const amt = parseFloat(subletInfo.SubletAmount || 0);
      lineOut.part_type = "PAS";
      lineOut.part_qty = 1;
      lineOut.act_price = isNaN(amt) ? 0 : amt;
    }

    // Primary labor
    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 = opCodeKey && opCodes?.[opCodeKey]?.desc ? opCodes[opCodeKey].desc : null;

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

    // Refinish (secondary fields, add amount)
    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.LaborType || "LAR";
      lineOut.lbr_hrs_j = isNaN(rHrs) ? 0 : rHrs;
      lineOut.lbr_op_j = refinishInfo.LaborOperation || null;
      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;
};

/**
 * Expand deletion IDs to include derived labor/refinish offsets.
 */
const extractDeletions = (deletions = {}) => {
  const items = Array.isArray(deletions.DamageLineInfo) ? deletions.DamageLineInfo : [deletions.DamageLineInfo || {}];
  const baseSeqs = items.map((line) => parseInt(line.UniqueSequenceNum, 10)).filter((id) => Number.isInteger(id));

  const allSeqs = [];
  for (const u of baseSeqs) allSeqs.push(u, u + 400000, u + 500000);
  return Array.from(new Set(allSeqs));
};

// S3 bucket + key builder (mirrors AddRq but with changeRequest prefix)
const ESTIMATE_XML_BUCKET =
  process.env?.NODE_ENV === "development"
    ? "parts-estimates"
    : InstanceMgr({
        imex: `imex-webest-xml`,
        rome: `rome-webest-xml`
      });

const buildEstimateXmlKey = (rq) => {
  const shopId = rq.ShopID;
  const jobId = rq.JobID;
  const ts = new Date().toISOString().replace(/:/g, "-");
  return `changeRequest/${shopId}/${jobId}/${ts}.xml`;
};

/**
 * Convert a full jobline object into a jobs_set_input for update_by_pk (omit immutable fields).
 */
const toJoblineSetInput = (jl) => {
  const {
    // immutable identity fields:
    // jobid,
    // unq_seq,
    // everything else:
    line_no,
    status,
    line_desc,
    manual_line,
    notes,
    part_qty,
    oem_partno,
    alt_partno,
    part_type,
    act_price,
    db_price,
    tax_part,
    mod_lbr_ty,
    mod_lb_hrs,
    op_code_desc,
    lbr_amt,
    lbr_typ_j,
    lbr_hrs_j,
    lbr_op_j,
    paint_stg,
    paint_tone
  } = jl;

  return {
    line_no,
    status,
    line_desc,
    manual_line,
    notes,
    part_qty,
    oem_partno,
    alt_partno,
    part_type,
    act_price,
    db_price,
    tax_part,
    mod_lbr_ty,
    mod_lb_hrs,
    op_code_desc,
    lbr_amt,
    lbr_typ_j,
    lbr_hrs_j,
    lbr_op_j,
    paint_stg,
    paint_tone
  };
};

/**
 * Handles VehicleDamageEstimateChgRq requests:
 * - Update core job fields
 * - For lines: update by PK if existing; otherwise bulk insert
 * - Soft-delete only explicit deletions (exclude any updated seqs)
 */
const partsManagementVehicleDamageEstimateChgRq = 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.VehicleDamageEstimateChgRq);

    const jobId = rq.JobID;
    const shopId = rq.ShopID;

    // Fire-and-forget archival on valid request
    (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", jobId, null, { key, bytes: rawXml?.length || 0 });
      } catch (e) {
        logger.log("parts-estimate-xml-upload-failed", "warn", jobId, null, { error: e?.message });
      }
    })();

    const job = await findJob(shopId, jobId, logger);
    if (!job) return res.status(404).send("Job not found");

    // --- Updated seqs from incoming changes ---
    const linesIn = Array.isArray(rq.AddsChgs?.DamageLineInfo)
      ? rq.AddsChgs.DamageLineInfo
      : [rq.AddsChgs?.DamageLineInfo || {}];

    const updatedSeqs = Array.from(
      new Set((linesIn || []).map((l) => parseInt(l?.UniqueSequenceNum || 0, 10)).filter((v) => Number.isInteger(v)))
    );

    // --- Fetch current notes for merge ---
    let currentJobLineNotes = {};
    if (updatedSeqs.length > 0) {
      const resp = await client.request(GET_JOBLINES_NOTES_BY_JOBID_UNQSEQ, { jobid: job.id, unqSeqs: updatedSeqs });
      if (resp?.joblines) {
        for (const jl of resp.joblines) currentJobLineNotes[jl.unq_seq] = jl.notes;
      }
    }

    const updatedJobData = extractUpdatedJobData(rq);
    const updatedLines = extractUpdatedJobLines(rq.AddsChgs, job.id, currentJobLineNotes);

    // --- Look up existing rows (by natural key) to decide update vs insert ---
    let existingIdByUnqSeq = {};
    if (updatedSeqs.length > 0) {
      const existing = await client.request(GET_JOBLINE_IDS_BY_JOBID_UNQSEQ, { jobid: job.id, unqSeqs: updatedSeqs });
      if (existing?.joblines) {
        for (const row of existing.joblines) existingIdByUnqSeq[row.unq_seq] = row.id;
      }
    }

    const toUpdate = [];
    const toInsert = [];
    for (const jl of updatedLines) {
      const id = existingIdByUnqSeq[jl.unq_seq];
      if (id) toUpdate.push({ id, _set: toJoblineSetInput(jl) });
      else toInsert.push(jl);
    }

    // Build deletions list and exclude any seqs we are updating (avoid accidental removal)
    const deletedLineIdsAll = extractDeletions(rq.Deletions);
    const deletionSeqs = deletedLineIdsAll.filter((u) => !updatedSeqs.includes(u));

    // Mutations:
    const updateJobPromise = client.request(UPDATE_JOB_BY_ID, { id: job.id, job: updatedJobData });

    const softDeletePromise = deletionSeqs.length
      ? client.request(SOFT_DELETE_JOBLINES_BY_IDS, { jobid: job.id, unqSeqs: deletionSeqs })
      : Promise.resolve({});

    // Update each existing row by primary key (parallelized)
    const perRowUpdatesPromise =
      toUpdate.length > 0
        ? Promise.all(toUpdate.map(({ id, _set }) => client.request(UPDATE_JOBLINE_BY_PK, { id, jl: _set })))
        : Promise.resolve([]);

    // Insert brand-new rows in bulk
    const insertPromise =
      toInsert.length > 0 ? client.request(INSERT_JOBLINES, { joblines: toInsert }) : Promise.resolve({});

    await Promise.all([updateJobPromise, softDeletePromise, perRowUpdatesPromise, insertPromise]);

    logger.log("parts-job-changed", "info", job.id, null);
    return res.status(200).json({ success: true, jobId: job.id });
  } catch (err) {
    logger.log("parts-chgrq-error", "error", null, null, { error: err });
    return res.status(err.status || 500).json({ error: err.message || "Internal error" });
  }
};

module.exports = partsManagementVehicleDamageEstimateChgRq;
