const CreateRRLogEvent = require("./rr-logger-event");
const { rrCombinedSearch, rrGetAdvisors, buildClientAndOpts } = require("./rr-lookup");
const { QueryJobData, buildRogogFromAllocations, buildRolaborFromRogog } = require("./rr-job-helpers");
const {
  exportJobToRR,
  createMinimalRRRepairOrder,
  updateRRRepairOrderWithFullData,
  finalizeRRRepairOrder
} = require("./rr-job-export");
const RRCalculateAllocations = require("./rr-calculate-allocations").default;
const { createRRCustomer } = require("./rr-customers");
const { ensureRRServiceVehicle } = require("./rr-service-vehicles");
const { classifyRRVendorError } = require("./rr-errors");
const { markRRExportSuccess, insertRRFailedExportLog } = require("./rr-export-logs");
const { withRRRequestXml, extractRRXmlPair } = require("./rr-log-xml");
const {
  makeVehicleSearchPayloadFromJob,
  ownersFromVinBlocks,
  readAdvisorNo,
  getTransactionType,
  normalizeCustomerCandidates,
  defaultRRTTL,
  RRCacheEnums
} = require("./rr-utils");
const { resolveRROpCodeFromBodyshop } = require("./rr-utils");
const { GraphQLClient } = require("graphql-request");
const queries = require("../graphql-client/queries");

/**
 * Advisors cache TTL (7 days)
 * @type {number}
 */
const ADVISORS_CACHE_TTL = 7 * 24 * 60 * 60; // seconds

/**
 * Resolve job ID from various shapes
 * @param explicit
 * @param payload
 * @param job
 * @returns {*|null}
 */
const resolveJobId = (explicit, payload, job) => explicit || payload?.jobId || job?.id || null;

/**
 * Resolve VIN from tx/job shapes
 * @param tx
 * @param job
 * @returns {*|null}
 */
const resolveVin = ({ tx, job }) => tx?.jobData?.vin || job?.v_vin || null;

/**
 * Add request/response XML to socket event payloads when available.
 * @param rrObj
 * @param payload
 * @returns {*}
 */
const withRRXmlSocketPayload = (rrObj, payload = {}) => {
  const { requestXml, responseXml } = extractRRXmlPair(rrObj);
  return {
    ...payload,
    ...(requestXml ? { requestXml } : {}),
    ...(responseXml ? { responseXml } : {})
  };
};

/**
 * Sort vehicle owners first in the list, preserving original order otherwise.
 * @param list
 * @returns {*}
 */
const sortVehicleOwnerFirst = (list) =>
  list
    .map((v, i) => ({ v, i }))
    .sort((a, b) => {
      const ao = a.v?.isVehicleOwner ? 1 : 0;
      const bo = b.v?.isVehicleOwner ? 1 : 0;
      if (ao !== bo) return bo - ao;
      return a.i - b.i;
    })
    .map(({ v }) => v);

/**
 * Merge customer candidates by custNo, combining isVehicleOwner flags and filling missing fields.
 * @param items
 * @returns {any[]}
 */
const mergeByCustNo = (items = []) => {
  const byId = new Map();
  for (const c of items) {
    const id = (c?.custNo || "").trim();
    if (!id) continue;
    const prev = byId.get(id);
    if (!prev) {
      byId.set(id, { ...c, isVehicleOwner: !!(c.vinOwner || c.isVehicleOwner) });
    } else {
      byId.set(id, {
        ...prev,
        name: prev.name || c.name,
        isVehicleOwner: !!(prev.isVehicleOwner || prev.vinOwner || c.isVehicleOwner || c.vinOwner),
        vinOwner: !!(prev.vinOwner || c.vinOwner || prev.isVehicleOwner || c.isVehicleOwner),
        address: prev.address || c.address
      });
    }
  }
  return Array.from(byId.values());
};

/**
 * Get session data or socket fallback
 * @param redisHelpers
 * @param socket
 * @returns {Promise<{bodyshopId: *, email: *, sess: null}>}
 */
const getSessionOrSocket = async (redisHelpers, socket) => {
  let sess = null;
  try {
    sess = await redisHelpers.getSessionData(socket.id);
  } catch {
    //
  }
  const bodyshopId = sess?.bodyshopId ?? socket.bodyshopId;
  const email = sess?.email ?? socket.user?.email;
  if (!bodyshopId) throw new Error("No bodyshopId (session/socket)");
  return { bodyshopId, email, sess };
};

/**
 * Fetch bodyshop data for socket
 * @param bodyshopId
 * @param socket
 * @returns {Promise<{id: string, intellipay_config: {payment_map: {amex: string}}}|{id: string, intellipay_config: null}|*>}
 */
const getBodyshopForSocket = async ({ bodyshopId, socket }) => {
  const endpoint = process.env.GRAPHQL_ENDPOINT;
  if (!endpoint) throw new Error("GRAPHQL_ENDPOINT not configured");
  const token = (socket?.data && socket.data.authToken) || (socket?.handshake?.auth && socket.handshake.auth.token);
  const client = new GraphQLClient(endpoint, {});
  const res = await client.setHeaders({ Authorization: `Bearer ${token}` }).request(queries.GET_BODYSHOP_BY_ID, {
    id: bodyshopId
  });
  const bodyshop = res?.bodyshops_by_pk;
  if (!bodyshop) throw new Error(`Bodyshop not found: ${bodyshopId}`);
  return bodyshop;
};

/**
 * GraphQL mutation to set job.dms_id, dms_customer_id, and dms_advisor_id
 * @param socket
 * @param jobId
 * @param dmsId
 * @param dmsCustomerId
 * @param dmsAdvisorId
 * @returns {Promise<void>}
 */
const setJobDmsIdForSocket = async ({ socket, jobId, dmsId, dmsCustomerId, dmsAdvisorId, mileageIn }) => {
  if (!jobId || !dmsId) {
    CreateRRLogEvent(socket, "WARN", "setJobDmsIdForSocket called without jobId or dmsId", {
      jobId,
      dmsId
    });
    return;
  }

  try {
    const endpoint = process.env.GRAPHQL_ENDPOINT;
    if (!endpoint) throw new Error("GRAPHQL_ENDPOINT not configured");

    const token = (socket?.data && socket.data.authToken) || (socket?.handshake?.auth && socket.handshake.auth.token);
    if (!token) throw new Error("Missing auth token for setJobDmsIdForSocket");

    const client = new GraphQLClient(endpoint, {});
    await client.setHeaders({ Authorization: `Bearer ${token}` }).request(queries.SET_JOB_DMS_ID, {
      id: jobId,
      dms_id: String(dmsId),
      dms_customer_id: dmsCustomerId ? String(dmsCustomerId) : null,
      dms_advisor_id: dmsAdvisorId ? String(dmsAdvisorId) : null,
      kmin: mileageIn != null && mileageIn > 0 ? parseInt(mileageIn, 10) : null
    });

    CreateRRLogEvent(socket, "INFO", "Linked job.dms_id to RR RO", {
      jobId,
      dmsId: String(dmsId),
      dmsCustomerId,
      dmsAdvisorId,
      mileageIn
    });
  } catch (err) {
    CreateRRLogEvent(socket, "ERROR", "Failed to set job.dms_id after RR create/update", {
      jobId,
      dmsId,
      dmsCustomerId,
      dmsAdvisorId,
      mileageIn,
      message: err?.message || String(err),
      stack: err?.stack
    });
  }
};
/**
 * Build advisors cache namespace and field
 * @param bodyshopId
 * @param routing
 * @param departmentType
 * @returns {{ns: string, field: string}}
 */
const buildAdvisorsCacheNS = ({ bodyshopId, routing, departmentType = "B" }) => {
  const dealer = routing?.dealerNumber || "unknown";
  const store = routing?.storeNumber || "none";
  const area = routing?.areaNumber || "none";
  const dept = (departmentType || "B").toUpperCase();
  return {
    ns: `rr:advisors:${bodyshopId}:${dealer}:${store}:${area}`,
    field: `dept:${dept}`
  };
};

/**
 * Run multi-query customer search (Full Name + VIN) and merge results.
 * @param bodyshop
 * @param job
 * @param socket
 * @param redisHelpers
 * @returns {Promise<*|*[]>}
 */
const rrMultiCustomerSearch = async ({ bodyshop, job, socket, redisHelpers }) => {
  const queriesList = [];

  // 1) Full Name (preferred)
  const firstName = job?.ownr_fn && String(job.ownr_fn).trim();
  const lastName = job?.ownr_ln && String(job.ownr_ln).trim();
  const company = job?.ownr_co_nm && String(job.ownr_co_nm).trim();

  if (firstName || lastName) {
    queriesList.push({
      q: { kind: "name", name: { fname: firstName || undefined, lname: lastName || undefined }, maxResults: 50 },
      fromVin: false
    });
  } else if (company) {
    queriesList.push({ q: { kind: "name", name: { name: company }, maxResults: 50 }, fromVin: false });
  }

  // 2) VIN (owner association)
  const vehQ = makeVehicleSearchPayloadFromJob(job);
  if (vehQ && vehQ.kind === "vin") queriesList.push({ q: vehQ, fromVin: true });

  if (!queriesList.length) return [];

  let ownersSet = null;
  const merged = [];

  for (const { q, fromVin } of queriesList) {
    try {
      CreateRRLogEvent(socket, "DEBUG", `{RR-SEARCH} Executing ${q.kind} query`, { q });

      const multiResponse = await rrCombinedSearch(bodyshop, q);

      CreateRRLogEvent(
        socket,
        "SILLY",
        "Multi Customer Search - raw combined search",
        withRRRequestXml(multiResponse, { response: multiResponse })
      );

      if (fromVin) {
        const multiBlocks = Array.isArray(multiResponse?.data) ? multiResponse.data : [];
        ownersSet = ownersFromVinBlocks(multiBlocks, job?.v_vin);
        try {
          await redisHelpers.setSessionTransactionData(
            socket.id,
            getTransactionType(job.id),
            RRCacheEnums.VINCandidates,
            multiBlocks,
            defaultRRTTL
          );
        } catch {
          /* ignore cache write issues */
        }
      }

      const norm = normalizeCustomerCandidates(multiResponse, { ownersSet });
      merged.push(...norm);
    } catch (e) {
      CreateRRLogEvent(socket, "WARN", "Multi-search subquery failed", withRRRequestXml(e, { kind: q.kind, error: e.message }));
    }
  }

  // NEW: dedupe across queries (name + vin)
  const deduped = mergeByCustNo(merged);

  return sortVehicleOwnerFirst(deduped);
};

/**
 * Register RR socket events
 * @param socket
 * @param redisHelpers
 */
const registerRREvents = ({ socket, redisHelpers }) => {
  socket.on("rr-lookup-combined", async ({ jobid, params } = {}, cb) => {
    try {
      const { bodyshopId } = await getSessionOrSocket(redisHelpers, socket);
      const bodyshop = await getBodyshopForSocket({ bodyshopId, socket });

      CreateRRLogEvent(socket, "DEBUG", "rr-lookup-combined: begin", { jobid, params });

      const response = await rrCombinedSearch(bodyshop, params || {});

      CreateRRLogEvent(socket, "SILLY", "rr-lookup-combined: received response", {
        response
      });

      let ownersSet = null;

      if ((params?.kind || "").toLowerCase() === "vin") {
        const blocks = Array.isArray(response?.data) ? response.data : [];
        ownersSet = ownersFromVinBlocks(blocks);
      }

      const normalized = sortVehicleOwnerFirst(normalizeCustomerCandidates(response, { ownersSet }));
      const rid = resolveJobId(jobid, { jobid }, null);
      const decorated = normalized.map((c) => (c.vinOwner != null ? c : { ...c, vinOwner: !!c.isVehicleOwner }));

      cb?.({ jobid: rid, data: decorated });

      socket.emit("rr-select-customer", decorated);

      CreateRRLogEvent(socket, "DEBUG", "rr-lookup-combined: emitted rr-select-customer", {
        count: decorated.length
      });
    } catch (e) {
      CreateRRLogEvent(socket, "ERROR", "RR combined lookup error", withRRRequestXml(e, { error: e.message, jobid }));
      cb?.({ jobid, error: e.message });
    }
  });

  socket.on("rr-get-advisors", async (args = {}, ack) => {
    const refresh = !!args?.refresh;
    const requestedDept = (args?.departmentType || "B").toUpperCase();

    try {
      const { bodyshopId } = await getSessionOrSocket(redisHelpers, socket);
      const bodyshop = await getBodyshopForSocket({ bodyshopId, socket });
      CreateRRLogEvent(socket, "DEBUG", "rr-get-advisors: begin", { args });

      // Build routing to bind cache key to bodyshop + dealer/store/area
      const { client, opts } = await buildClientAndOpts(bodyshop);
      const routing = opts?.routing || client?.opts?.routing || {};
      if (!routing?.dealerNumber) {
        throw new Error("rr-get-advisors: routing.dealerNumber required");
      }

      const { ns, field } = buildAdvisorsCacheNS({
        bodyshopId,
        routing,
        departmentType: requestedDept
      });

      let result = null;
      let fromCache = false;

      // 1) Try cache (unless forced refresh)
      if (!refresh) {
        try {
          const cached = await redisHelpers.getProviderCache(ns, field);
          if (cached && Array.isArray(cached)) {
            result = cached;
            fromCache = true;
            CreateRRLogEvent(socket, "DEBUG", "rr-get-advisors: cache hit", {
              ns,
              field,
              count: cached.length,
              ttl: ADVISORS_CACHE_TTL
            });
          }
        } catch (e) {
          CreateRRLogEvent(socket, "WARN", "rr-get-advisors: cache read failed", { ns, field, error: e?.message });
        }
      }

      // 2) Fetch + cache when no cache or forced refresh
      if (!result) {
        const getAdvisorsCall = await rrGetAdvisors(bodyshop, { departmentType: requestedDept });
        result = Array.isArray(getAdvisorsCall?.data) ? getAdvisorsCall.data : [];
        try {
          await redisHelpers.setProviderCache(ns, field, result, ADVISORS_CACHE_TTL);
          CreateRRLogEvent(socket, "SILLY", "rr-get-advisors: fetched live data", {
            getAdvisorsCall
          });
          CreateRRLogEvent(socket, "DEBUG", "rr-get-advisors: cache populated", {
            ns,
            field,
            count: result.length,
            ttl: ADVISORS_CACHE_TTL
          });
        } catch (e) {
          CreateRRLogEvent(socket, "WARN", "rr-get-advisors: cache write failed", { ns, field, error: e?.message });
        }
      }

      // 3) Respond
      ack?.({ ok: true, result, fromCache });
      socket.emit("rr-get-advisors:result", { result, fromCache });
      CreateRRLogEvent(socket, "DEBUG", "rr-get-advisors: success", {
        count: Array.isArray(result) ? result.length : undefined,
        fromCache
      });
    } catch (err) {
      CreateRRLogEvent(socket, "ERROR", "rr-get-advisors: failed", withRRRequestXml(err, { error: err?.message }));
      ack?.({ ok: false, error: err?.message || "get advisors failed" });
    }
  });

  /**
   * NEW: Early RO Creation Event
   * Creates a minimal RO from convert button or admin page with customer selection,
   * advisor, mileage, and optional story/overrides.
   */
  socket.on("rr-create-early-ro", async ({ jobid, jobId, txEnvelope } = {}) => {
    const rid = resolveJobId(jobid || jobId, { jobId, jobid }, null);

    try {
      if (!rid) throw new Error("RR early create: jobid required");

      CreateRRLogEvent(socket, "DEBUG", `{EARLY-1} Received RR early RO creation request`, { jobid: rid });

      // Cache txEnvelope (contains advisor, mileage, story, overrides)
      await redisHelpers.setSessionTransactionData(
        socket.id,
        getTransactionType(rid),
        RRCacheEnums.txEnvelope,
        txEnvelope || {},
        defaultRRTTL
      );

      CreateRRLogEvent(socket, "DEBUG", `{EARLY-1.1} Cached txEnvelope`, { hasTxEnvelope: !!txEnvelope });

      const job = await QueryJobData({ redisHelpers }, rid);
      await redisHelpers.setSessionTransactionData(
        socket.id,
        getTransactionType(rid),
        RRCacheEnums.JobData,
        job,
        defaultRRTTL
      );

      CreateRRLogEvent(socket, "DEBUG", `{EARLY-1.2} Cached JobData`, { vin: job?.v_vin, ro: job?.ro_number });

      const adv = readAdvisorNo(
        { txEnvelope },
        await redisHelpers.getSessionTransactionData(socket.id, getTransactionType(rid), RRCacheEnums.AdvisorNo)
      );

      if (adv) {
        await redisHelpers.setSessionTransactionData(
          socket.id,
          getTransactionType(rid),
          RRCacheEnums.AdvisorNo,
          String(adv),
          defaultRRTTL
        );

        CreateRRLogEvent(socket, "DEBUG", `{EARLY-1.3} Cached advisorNo`, { advisorNo: String(adv) });
      }

      const { bodyshopId } = await getSessionOrSocket(redisHelpers, socket);
      const bodyshop = await getBodyshopForSocket({ bodyshopId, socket });

      CreateRRLogEvent(socket, "DEBUG", `{EARLY-2} Running multi-search (Full Name + VIN)`);

      const candidates = await rrMultiCustomerSearch({ bodyshop, job, socket, redisHelpers });
      const decorated = candidates.map((c) => (c.vinOwner != null ? c : { ...c, vinOwner: !!c.isVehicleOwner }));

      socket.emit("rr-select-customer", decorated);
      CreateRRLogEvent(socket, "DEBUG", `{EARLY-2.1} Emitted rr-select-customer for early RO`, {
        count: decorated.length,
        anyOwner: decorated.some((c) => c.vinOwner || c.isVehicleOwner)
      });
    } catch (error) {
      CreateRRLogEvent(
        socket,
        "ERROR",
        `Error during RR early RO creation (prepare)`,
        withRRRequestXml(error, {
          error: error.message,
          stack: error.stack,
          jobid: rid
        })
      );

      try {
        socket.emit(
          "export-failed",
          withRRXmlSocketPayload(error, {
            vendor: "rr",
            jobId: rid,
            error: error.message
          })
        );
      } catch {
        //
      }
    }
  });

  /**
   * NEW: Early RO Customer Selected Event
   * Handles customer selection for early RO creation and creates minimal RO.
   */
  socket.on("rr-early-customer-selected", async ({ jobid, jobId, selectedCustomerId, custNo, create } = {}, ack) => {
    const rid = resolveJobId(jobid || jobId, { jobid, jobId }, null);
    let bodyshop = null;
    let job = null;
    let createdCustomer = false;

    try {
      if (!rid) throw new Error("jobid required");
      CreateRRLogEvent(socket, "DEBUG", `{EARLY-3} rr-early-customer-selected`, {
        jobid: rid,
        custNo,
        selectedCustomerId,
        create: !!create
      });

      const ns = getTransactionType(rid);

      CreateRRLogEvent(socket, "DEBUG", `{EARLY-3.0a} Raw parameters received`, {
        custNo: custNo,
        custNoType: typeof custNo,
        selectedCustomerId: selectedCustomerId,
        create: create
      });

      let selectedCustNo =
        (custNo && String(custNo)) ||
        (selectedCustomerId && String(selectedCustomerId)) ||
        (await redisHelpers.getSessionTransactionData(socket.id, ns, RRCacheEnums.SelectedCustomer));

      CreateRRLogEvent(socket, "DEBUG", `{EARLY-3.0b} After initial resolution`, {
        selectedCustNo,
        selectedCustNoType: typeof selectedCustNo
      });

      // Filter out invalid values
      if (
        selectedCustNo === "undefined" ||
        selectedCustNo === "null" ||
        (selectedCustNo && selectedCustNo.trim() === "")
      ) {
        selectedCustNo = null;
      }

      CreateRRLogEvent(socket, "DEBUG", `{EARLY-3.0} Resolved customer selection`, {
        selectedCustNo,
        willCreateNew: create === true || !selectedCustNo
      });

      job = await redisHelpers.getSessionTransactionData(socket.id, ns, RRCacheEnums.JobData);

      const txEnvelope = (await redisHelpers.getSessionTransactionData(socket.id, ns, RRCacheEnums.txEnvelope)) || {};

      if (!job) throw new Error("Staged JobData not found (run rr-create-early-ro first).");

      const { bodyshopId } = await getSessionOrSocket(redisHelpers, socket);

      bodyshop = await getBodyshopForSocket({ bodyshopId, socket });

      // Create customer (if requested or none chosen)
      if (create === true || !selectedCustNo) {
        CreateRRLogEvent(socket, "DEBUG", `{EARLY-3.1} Creating RR customer`);

        const created = await createRRCustomer({ bodyshop, job, socket });
        selectedCustNo = String(created?.customerNo || "");

        CreateRRLogEvent(socket, "DEBUG", `{EARLY-3.2} Created customer`, {
          custNo: selectedCustNo,
          createdCustomerNo: created?.customerNo
        });

        if (!selectedCustNo || selectedCustNo === "undefined" || selectedCustNo.trim() === "") {
          throw new Error("RR create customer returned no valid custNo");
        }

        createdCustomer = true;
      }

      // VIN owner pre-check
      try {
        const vehQ = makeVehicleSearchPayloadFromJob(job);
        if (vehQ && vehQ.kind === "vin" && job?.v_vin) {
          const vinResponse = await rrCombinedSearch(bodyshop, vehQ);

          CreateRRLogEvent(
            socket,
            "SILLY",
            `VIN owner pre-check response (early RO)`,
            withRRRequestXml(vinResponse, { response: vinResponse })
          );

          const vinBlocks = Array.isArray(vinResponse?.data) ? vinResponse.data : [];

          try {
            await redisHelpers.setSessionTransactionData(
              socket.id,
              ns,
              RRCacheEnums.VINCandidates,
              vinBlocks,
              defaultRRTTL
            );
          } catch {
            //
          }

          const ownersSet = ownersFromVinBlocks(vinBlocks, job.v_vin);

          if (ownersSet?.size) {
            const sel = String(selectedCustNo);

            if (!ownersSet.has(sel)) {
              const [existingOwner] = Array.from(ownersSet).map(String);
              CreateRRLogEvent(socket, "DEBUG", `{EARLY-3.2a} VIN exists; switching to VIN owner`, {
                vin: job.v_vin,
                selected: sel,
                existingOwner
              });
              selectedCustNo = existingOwner;
            }
          }
        }
      } catch (e) {
        CreateRRLogEvent(
          socket,
          "WARN",
          `VIN owner pre-check failed; continuing with selected customer (early RO)`,
          withRRRequestXml(e, {
            error: e?.message
          })
        );
      }

      // Cache final/effective customer selection
      const effectiveCustNo = String(selectedCustNo);

      await redisHelpers.setSessionTransactionData(
        socket.id,
        ns,
        RRCacheEnums.SelectedCustomer,
        effectiveCustNo,
        defaultRRTTL
      );

      CreateRRLogEvent(socket, "DEBUG", `{EARLY-3.3} Cached selected customer`, { custNo: effectiveCustNo });

      // Build client & routing
      const { client, opts } = await buildClientAndOpts(bodyshop);
      const routing = opts?.routing || client?.opts?.routing || null;
      if (!routing?.dealerNumber) throw new Error("ensureRRServiceVehicle: routing.dealerNumber required");

      // Reconstruct a lightweight tx object
      const tx = {
        jobData: {
          ...job,
          vin: job?.v_vin
        },
        txEnvelope
      };

      const vin = resolveVin({ tx, job });

      if (!vin) {
        CreateRRLogEvent(socket, "ERROR", "{EARLY-3.x} No VIN found for ensureRRServiceVehicle", { jobid: rid });
        throw new Error("ensureRRServiceVehicle: vin required");
      }

      CreateRRLogEvent(socket, "DEBUG", "{EARLY-3.4} ensureRRServiceVehicle: starting", {
        jobid: rid,
        selectedCustomerNo: effectiveCustNo,
        vin,
        dealerNumber: routing.dealerNumber,
        storeNumber: routing.storeNumber,
        areaNumber: routing.areaNumber
      });

      const ensured = await ensureRRServiceVehicle({
        client,
        routing,
        bodyshop,
        selectedCustomerNo: effectiveCustNo,
        custNo: effectiveCustNo,
        customerNo: effectiveCustNo,
        vin,
        job,
        socket,
        redisHelpers
      });

      CreateRRLogEvent(socket, "DEBUG", "{EARLY-3.5} ensureRRServiceVehicle: done", ensured);

      const cachedAdvisor = await redisHelpers.getSessionTransactionData(socket.id, ns, RRCacheEnums.AdvisorNo);
      const advisorNo = readAdvisorNo({ txEnvelope }, cachedAdvisor);

      if (!advisorNo) {
        CreateRRLogEvent(socket, "ERROR", `Advisor is required (advisorNo) for early RO`);
        await insertRRFailedExportLog({
          socket,
          jobId: rid,
          job,
          bodyshop,
          error: new Error("Advisor is required (advisorNo)."),
          classification: { errorCode: "RR_MISSING_ADVISOR", friendlyMessage: "Advisor is required." }
        });
        socket.emit("export-failed", { vendor: "rr", jobId: rid, error: "Advisor is required (advisorNo)." });
        return ack?.({ ok: false, error: "Advisor is required (advisorNo)." });
      }

      await redisHelpers.setSessionTransactionData(
        socket.id,
        ns,
        RRCacheEnums.AdvisorNo,
        String(advisorNo),
        defaultRRTTL
      );

      // CREATE MINIMAL RO (early creation)
      CreateRRLogEvent(socket, "DEBUG", `{EARLY-4} Creating minimal RR RO`);
      const result = await createMinimalRRRepairOrder({
        bodyshop,
        job,
        selectedCustomer: { customerNo: effectiveCustNo, custNo: effectiveCustNo },
        advisorNo: String(advisorNo),
        txEnvelope,
        socket,
        svId: ensured?.svId || null
      });

      // Cache raw export result + pending RO number
      await redisHelpers.setSessionTransactionData(
        socket.id,
        ns,
        RRCacheEnums.ExportResult,
        result || {},
        defaultRRTTL
      );

      if (result?.success) {
        const data = result?.data || {};

        // Prefer explicit return from export function; then fall back to fields
        const dmsRoNo = result?.roNo ?? data?.dmsRoNo ?? null;

        const outsdRoNo = data?.outsdRoNo ?? job?.ro_number ?? job?.id ?? null;

        CreateRRLogEvent(
          socket,
          "DEBUG",
          "Early RO created - checking dmsRoNo",
          withRRRequestXml(result, {
            dmsRoNo,
            resultRoNo: result?.roNo,
            dataRoNo: data?.dmsRoNo,
            jobId: rid
          })
        );

        // ✅ Persist DMS RO number, customer ID, advisor ID, and mileage on the job
        if (dmsRoNo) {
          const mileageIn = txEnvelope?.kmin ?? null;
          CreateRRLogEvent(socket, "DEBUG", "Calling setJobDmsIdForSocket", {
            jobId: rid,
            dmsId: dmsRoNo,
            customerId: effectiveCustNo,
            advisorId: String(advisorNo),
            mileageIn
          });
          await setJobDmsIdForSocket({
            socket,
            jobId: rid,
            dmsId: dmsRoNo,
            dmsCustomerId: effectiveCustNo,
            dmsAdvisorId: String(advisorNo),
            mileageIn
          });
        } else {
          CreateRRLogEvent(
            socket,
            "WARN",
            "RR early RO creation succeeded but no DMS RO number was returned",
            withRRRequestXml(result, {
              jobId: rid,
              resultPreview: {
                roNo: result?.roNo,
                data: {
                  dmsRoNo: data?.dmsRoNo,
                  outsdRoNo: data?.outsdRoNo
                }
              }
            })
          );
        }

        await redisHelpers.setSessionTransactionData(
          socket.id,
          ns,
          RRCacheEnums.PendingRO,
          {
            outsdRoNo,
            dmsRoNo,
            customerNo: String(effectiveCustNo),
            advisorNo: String(advisorNo),
            vin: job?.v_vin || null,
            earlyRoCreated: true // Flag to indicate this was an early RO
          },
          defaultRRTTL
        );

        CreateRRLogEvent(
          socket,
          "INFO",
          `{EARLY-5} Minimal RO created successfully`,
          withRRRequestXml(result, {
            dmsRoNo: dmsRoNo || null,
            outsdRoNo: outsdRoNo || null
          })
        );

        // Mark success in export logs
        await markRRExportSuccess({
          socket,
          jobId: rid,
          job,
          bodyshop,
          result,
          isEarlyRo: true
        });

        // Tell FE that early RO was created
        socket.emit("rr-early-ro-created", { jobId: rid, dmsRoNo, outsdRoNo });

        // Emit result
        socket.emit("rr-create-early-ro:result", { jobId: rid, bodyshopId: bodyshop?.id, result });

        // ACK with RO details
        ack?.({
          ok: true,
          dmsRoNo,
          outsdRoNo,
          result,
          custNo: String(effectiveCustNo),
          createdCustomer,
          earlyRoCreated: true
        });
      } else {
        // classify & fail
        const tx = result?.statusBlocks?.transaction;

        const vendorStatusCode = Number(
          result?.roStatus?.statusCode ?? result?.roStatus?.StatusCode ?? tx?.statusCode ?? tx?.StatusCode
        );

        const vendorMessage =
          result?.roStatus?.message ??
          result?.roStatus?.Message ??
          tx?.message ??
          tx?.Message ??
          result?.error ??
          "RR early RO creation failed";

        const cls = classifyRRVendorError({
          code: vendorStatusCode,
          message: vendorMessage
        });

        CreateRRLogEvent(
          socket,
          "ERROR",
          `Early RO creation failed`,
          withRRRequestXml(result, {
            roStatus: result?.roStatus,
            statusBlocks: result?.statusBlocks,
            classification: cls
          })
        );

        await insertRRFailedExportLog({
          socket,
          jobId: rid,
          job,
          bodyshop,
          error: new Error(cls.friendlyMessage || result?.error || "RR early RO creation failed"),
          classification: cls,
          result
        });

        socket.emit("export-failed", {
          ...withRRXmlSocketPayload(result, {
            vendor: "rr",
            jobId: rid,
            error: cls?.friendlyMessage || result?.error || "RR early RO creation failed"
          }),
          ...cls
        });

        ack?.({
          ok: false,
          error: cls.friendlyMessage || result?.error || "RR early RO creation failed",
          result,
          classification: cls
        });
      }
    } catch (error) {
      const cls = classifyRRVendorError(error);

      CreateRRLogEvent(
        socket,
        "ERROR",
        `Error during RR early RO creation (customer-selected)`,
        withRRRequestXml(error, {
          error: error.message,
          vendorStatusCode: cls.vendorStatusCode,
          code: cls.errorCode,
          friendly: cls.friendlyMessage,
          stack: error.stack,
          jobid: rid
        })
      );

      try {
        if (!bodyshop || !job) {
          const { bodyshopId } = await getSessionOrSocket(redisHelpers, socket);
          bodyshop = bodyshop || (await getBodyshopForSocket({ bodyshopId, socket }));
          job =
            job ||
            (await redisHelpers.getSessionTransactionData(socket.id, getTransactionType(rid), RRCacheEnums.JobData));
        }
      } catch {
        //
      }

      await insertRRFailedExportLog({
        socket,
        jobId: rid,
        job,
        bodyshop,
        error,
        classification: cls
      });

      try {
        socket.emit("export-failed", {
          ...withRRXmlSocketPayload(error, {
            vendor: "rr",
            jobId: rid,
            error: error.message
          }),
          ...cls
        });
        socket.emit("rr-user-notice", { jobId: rid, ...cls });
      } catch {
        //
      }

      ack?.({ ok: false, error: cls.friendlyMessage || error.message, classification: cls });
    }
  });

  socket.on("rr-export-job", async ({ jobid, jobId, txEnvelope } = {}, ack) => {
    const rid = resolveJobId(jobid || jobId, { jobId, jobid }, null);

    try {
      if (!rid) throw new Error("RR export: jobid required");

      CreateRRLogEvent(socket, "DEBUG", `{1} Received RR export request`, { jobid: rid });

      await redisHelpers.setSessionTransactionData(
        socket.id,
        getTransactionType(rid),
        RRCacheEnums.txEnvelope,
        txEnvelope || {},
        defaultRRTTL
      );

      CreateRRLogEvent(socket, "DEBUG", `{1.1} Cached txEnvelope`, { hasTxEnvelope: !!txEnvelope });

      const job = await QueryJobData({ redisHelpers }, rid);
      await redisHelpers.setSessionTransactionData(
        socket.id,
        getTransactionType(rid),
        RRCacheEnums.JobData,
        job,
        defaultRRTTL
      );

      CreateRRLogEvent(socket, "DEBUG", `{1.2} Cached JobData`, { vin: job?.v_vin, ro: job?.ro_number });

      const adv = readAdvisorNo(
        { txEnvelope },
        await redisHelpers.getSessionTransactionData(socket.id, getTransactionType(rid), RRCacheEnums.AdvisorNo)
      );

      if (adv) {
        await redisHelpers.setSessionTransactionData(
          socket.id,
          getTransactionType(rid),
          RRCacheEnums.AdvisorNo,
          String(adv),
          defaultRRTTL
        );

        CreateRRLogEvent(socket, "DEBUG", `{1.3} Cached advisorNo`, { advisorNo: String(adv) });
      }

      const { bodyshopId } = await getSessionOrSocket(redisHelpers, socket);
      const bodyshop = await getBodyshopForSocket({ bodyshopId, socket });

      // Check if this job already has an early RO - if so, use stored IDs and skip customer search
      const hasEarlyRO = !!job?.dms_id;

      if (hasEarlyRO) {
        CreateRRLogEvent(socket, "DEBUG", `{2} Early RO exists - using stored customer/advisor`, {
          dms_id: job.dms_id,
          dms_customer_id: job.dms_customer_id,
          dms_advisor_id: job.dms_advisor_id
        });

        // Cache the stored customer/advisor IDs for the next step
        if (job.dms_customer_id) {
          await redisHelpers.setSessionTransactionData(
            socket.id,
            getTransactionType(rid),
            RRCacheEnums.SelectedCustomer,
            String(job.dms_customer_id),
            defaultRRTTL
          );
        }
        if (job.dms_advisor_id) {
          await redisHelpers.setSessionTransactionData(
            socket.id,
            getTransactionType(rid),
            RRCacheEnums.AdvisorNo,
            String(job.dms_advisor_id),
            defaultRRTTL
          );
        }

        // Emit empty customer list to frontend (won't show modal)
        socket.emit("rr-select-customer", []);

        // Continue directly with the export by calling the selected customer handler logic inline
        // This is essentially the same as if user selected the stored customer
        const selectedCustNo = job.dms_customer_id;

        if (!selectedCustNo) {
          throw new Error("Early RO exists but no customer ID stored");
        }

        // Continue with ensureRRServiceVehicle and export (same as rr-selected-customer handler)
        const { client, opts } = await buildClientAndOpts(bodyshop);
        const routing = opts?.routing || client?.opts?.routing || null;
        if (!routing?.dealerNumber) throw new Error("ensureRRServiceVehicle: routing.dealerNumber required");

        const tx = {
          jobData: {
            ...job,
            vin: job?.v_vin
          },
          txEnvelope
        };

        const vin = resolveVin({ tx, job });
        if (!vin) {
          CreateRRLogEvent(socket, "ERROR", "{3.x} No VIN found for ensureRRServiceVehicle", { jobid: rid });
          throw new Error("ensureRRServiceVehicle: vin required");
        }

        const ensured = await ensureRRServiceVehicle({
          client,
          routing,
          bodyshop,
          selectedCustomerNo: String(selectedCustNo),
          custNo: String(selectedCustNo),
          customerNo: String(selectedCustNo),
          vin,
          job,
          socket,
          redisHelpers
        });

        const advisorNo =
          job.dms_advisor_id ||
          readAdvisorNo(
            { txEnvelope },
            await redisHelpers.getSessionTransactionData(socket.id, getTransactionType(rid), RRCacheEnums.AdvisorNo)
          );

        if (!advisorNo) {
          throw new Error("Advisor is required (advisorNo).");
        }

        // UPDATE existing RO with full data
        CreateRRLogEvent(socket, "DEBUG", `{4} Updating existing RR RO with full data`, { dmsRoNo: job.dms_id });
        const result = await updateRRRepairOrderWithFullData({
          bodyshop,
          job,
          selectedCustomer: { customerNo: String(selectedCustNo), custNo: String(selectedCustNo) },
          advisorNo: String(advisorNo),
          txEnvelope,
          socket,
          svId: ensured?.svId || null,
          roNo: job.dms_id
        });

        CreateRRLogEvent(
          socket,
          "SILLY",
          "{4.1} RR RO update response received",
          withRRRequestXml(result, {
            dmsRoNo: job.dms_id,
            success: !!result?.success
          })
        );

        if (!result?.success) {
          CreateRRLogEvent(
            socket,
            "ERROR",
            "RR Repair Order update failed",
            withRRRequestXml(result, {
              jobId: rid,
              dmsRoNo: job.dms_id,
              roStatus: result?.roStatus,
              statusBlocks: result?.statusBlocks
            })
          );
          throw new Error(result?.roStatus?.message || "Failed to update RR Repair Order");
        }

        const dmsRoNo = result?.roNo ?? result?.data?.dmsRoNo ?? job.dms_id;

        await redisHelpers.setSessionTransactionData(
          socket.id,
          getTransactionType(rid),
          RRCacheEnums.ExportResult,
          result || {},
          defaultRRTTL
        );

        await redisHelpers.setSessionTransactionData(
          socket.id,
          getTransactionType(rid),
          RRCacheEnums.PendingRO,
          {
            outsdRoNo: result?.data?.outsdRoNo ?? job?.ro_number ?? job?.id ?? null,
            dmsRoNo,
            customerNo: String(selectedCustNo),
            advisorNo: String(advisorNo),
            vin: job?.v_vin || null,
            isUpdate: true
          },
          defaultRRTTL
        );

        CreateRRLogEvent(
          socket,
          "INFO",
          `RR Repair Order updated successfully`,
          withRRRequestXml(result, {
            dmsRoNo,
            jobId: rid
          })
        );

        // For early RO flow, only emit validation-required (not export-job:result)
        // since the export is not complete yet - we're just waiting for validation
        socket.emit("rr-validation-required", { dmsRoNo, jobId: rid });

        return ack?.({ ok: true, skipCustomerSelection: true, dmsRoNo });
      }

      CreateRRLogEvent(socket, "DEBUG", `{2} Running multi-search (Full Name + VIN)`);

      const candidates = await rrMultiCustomerSearch({ bodyshop, job, socket, redisHelpers });
      const decorated = candidates.map((c) => (c.vinOwner != null ? c : { ...c, vinOwner: !!c.isVehicleOwner }));

      socket.emit("rr-select-customer", decorated);
      CreateRRLogEvent(socket, "DEBUG", `{2.1} Emitted rr-select-customer`, {
        count: decorated.length,
        anyOwner: decorated.some((c) => c.vinOwner || c.isVehicleOwner)
      });
    } catch (error) {
      CreateRRLogEvent(
        socket,
        "ERROR",
        `Error during RR export (prepare)`,
        withRRRequestXml(error, {
          error: error.message,
          stack: error.stack,
          jobid: rid
        })
      );

      try {
        socket.emit(
          "export-failed",
          withRRXmlSocketPayload(error, {
            vendor: "rr",
            jobId: rid,
            error: error.message
          })
        );
      } catch {
        //
      }
    }
  });

  socket.on("rr-selected-customer", async ({ jobid, jobId, selectedCustomerId, custNo, create } = {}, ack) => {
    const rid = resolveJobId(jobid || jobId, { jobid, jobId }, null);
    let bodyshop = null;
    let job = null;
    let createdCustomer = false;

    try {
      if (!rid) throw new Error("jobid required");
      CreateRRLogEvent(socket, "DEBUG", `{3} rr-selected-customer`, {
        jobid: rid,
        custNo,
        selectedCustomerId,
        create: !!create
      });

      const ns = getTransactionType(rid);

      let selectedCustNo =
        (custNo && String(custNo)) ||
        (selectedCustomerId && String(selectedCustomerId)) ||
        (await redisHelpers.getSessionTransactionData(socket.id, ns, RRCacheEnums.SelectedCustomer));

      job = await redisHelpers.getSessionTransactionData(socket.id, ns, RRCacheEnums.JobData);

      const txEnvelope = (await redisHelpers.getSessionTransactionData(socket.id, ns, RRCacheEnums.txEnvelope)) || {};

      if (!job) throw new Error("Staged JobData not found (run rr-export-job first).");

      const { bodyshopId } = await getSessionOrSocket(redisHelpers, socket);

      bodyshop = await getBodyshopForSocket({ bodyshopId, socket });

      // Create customer (if requested or none chosen)
      if (create === true || !selectedCustNo) {
        CreateRRLogEvent(socket, "DEBUG", `{3.1} Creating RR customer`);

        const created = await createRRCustomer({ bodyshop, job, socket });
        selectedCustNo = String(created?.customerNo);

        if (!selectedCustNo) throw new Error("RR create customer returned no custNo");

        createdCustomer = true;

        CreateRRLogEvent(socket, "DEBUG", `{3.2} Created customer`, { custNo: selectedCustNo });
      }

      // VIN owner pre-check
      try {
        const vehQ = makeVehicleSearchPayloadFromJob(job);
        if (vehQ && vehQ.kind === "vin" && job?.v_vin) {
          const vinResponse = await rrCombinedSearch(bodyshop, vehQ);

          CreateRRLogEvent(
            socket,
            "SILLY",
            `VIN owner pre-check response`,
            withRRRequestXml(vinResponse, { response: vinResponse })
          );

          const vinBlocks = Array.isArray(vinResponse?.data) ? vinResponse.data : [];

          try {
            await redisHelpers.setSessionTransactionData(
              socket.id,
              ns,
              RRCacheEnums.VINCandidates,
              vinBlocks,
              defaultRRTTL
            );
          } catch {
            //
          }

          const ownersSet = ownersFromVinBlocks(vinBlocks, job.v_vin);

          if (ownersSet?.size) {
            const sel = String(selectedCustNo);

            if (!ownersSet.has(sel)) {
              const [existingOwner] = Array.from(ownersSet).map(String);
              CreateRRLogEvent(socket, "DEBUG", `{3.2a} VIN exists; switching to VIN owner`, {
                vin: job.v_vin,
                selected: sel,
                existingOwner
              });
              selectedCustNo = existingOwner;
            }
          }
        }
      } catch (e) {
        CreateRRLogEvent(
          socket,
          "WARN",
          `VIN owner pre-check failed; continuing with selected customer`,
          withRRRequestXml(e, {
            error: e?.message
          })
        );
      }

      // Cache final/effective customer selection
      const effectiveCustNo = String(selectedCustNo);

      await redisHelpers.setSessionTransactionData(
        socket.id,
        ns,
        RRCacheEnums.SelectedCustomer,
        effectiveCustNo,
        defaultRRTTL
      );

      CreateRRLogEvent(socket, "DEBUG", `{3.3} Cached selected customer`, { custNo: effectiveCustNo });

      // Build client & routing
      const { client, opts } = await buildClientAndOpts(bodyshop);
      const routing = opts?.routing || client?.opts?.routing || null;
      if (!routing?.dealerNumber) throw new Error("ensureRRServiceVehicle: routing.dealerNumber required");

      // Reconstruct a lightweight tx object (so resolveVin can use the same shape we logged at {1.2})
      const tx = {
        jobData: {
          ...job,
          vin: job?.v_vin
        },
        txEnvelope
      };

      const vin = resolveVin({ tx, job });

      if (!vin) {
        CreateRRLogEvent(socket, "ERROR", "{3.x} No VIN found for ensureRRServiceVehicle", { jobid: rid });
        throw new Error("ensureRRServiceVehicle: vin required");
      }

      CreateRRLogEvent(socket, "DEBUG", "{3.2} ensureRRServiceVehicle: starting", {
        jobid: rid,
        selectedCustomerNo: effectiveCustNo,
        vin,
        dealerNumber: routing.dealerNumber,
        storeNumber: routing.storeNumber,
        areaNumber: routing.areaNumber
      });

      const ensured = await ensureRRServiceVehicle({
        client,
        routing,
        bodyshop,
        selectedCustomerNo: effectiveCustNo,
        custNo: effectiveCustNo,
        customerNo: effectiveCustNo,
        vin,
        job,
        socket,
        redisHelpers
      });

      CreateRRLogEvent(socket, "DEBUG", "{3.4} ensureRRServiceVehicle: done", ensured);

      const cachedAdvisor = await redisHelpers.getSessionTransactionData(socket.id, ns, RRCacheEnums.AdvisorNo);
      const advisorNo = readAdvisorNo({ txEnvelope }, cachedAdvisor);

      if (!advisorNo) {
        CreateRRLogEvent(socket, "ERROR", `Advisor is required (advisorNo)`);
        await insertRRFailedExportLog({
          socket,
          jobId: rid,
          job,
          bodyshop,
          error: new Error("Advisor is required (advisorNo)."),
          classification: { errorCode: "RR_MISSING_ADVISOR", friendlyMessage: "Advisor is required." }
        });
        socket.emit("export-failed", { vendor: "rr", jobId: rid, error: "Advisor is required (advisorNo)." });
        return ack?.({ ok: false, error: "Advisor is required (advisorNo)." });
      }

      await redisHelpers.setSessionTransactionData(
        socket.id,
        ns,
        RRCacheEnums.AdvisorNo,
        String(advisorNo),
        defaultRRTTL
      );

      // Check if this job already has an early RO created (check job.dms_id)
      // If so, we'll use stored customer/advisor IDs and do a full data UPDATE instead of CREATE
      const existingDmsId = job?.dms_id || null;
      const shouldUpdate = !!existingDmsId;

      // When updating an early RO, use stored customer/advisor IDs
      let finalEffectiveCustNo = effectiveCustNo;
      let finalAdvisorNo = advisorNo;

      if (shouldUpdate && job?.dms_customer_id) {
        CreateRRLogEvent(socket, "DEBUG", `Using stored customer ID from early RO`, {
          storedCustomerId: job.dms_customer_id,
          originalCustomerId: effectiveCustNo
        });
        finalEffectiveCustNo = String(job.dms_customer_id);
      }

      if (shouldUpdate && job?.dms_advisor_id) {
        CreateRRLogEvent(socket, "DEBUG", `Using stored advisor ID from early RO`, {
          storedAdvisorId: job.dms_advisor_id,
          originalAdvisorId: advisorNo
        });
        finalAdvisorNo = String(job.dms_advisor_id);
      }

      let result;

      if (shouldUpdate) {
        // UPDATE existing RO with full data
        CreateRRLogEvent(socket, "DEBUG", `{4} Updating existing RR RO with full data`, { dmsRoNo: existingDmsId });
        result = await updateRRRepairOrderWithFullData({
          bodyshop,
          job,
          selectedCustomer: { customerNo: finalEffectiveCustNo, custNo: finalEffectiveCustNo },
          advisorNo: String(finalAdvisorNo),
          txEnvelope,
          socket,
          svId: ensured?.svId || null,
          roNo: existingDmsId
        });
      } else {
        // CREATE new RO (legacy flow - full data on first create)
        CreateRRLogEvent(socket, "DEBUG", `{4} Performing RR create (step 1 - full data)`);
        result = await exportJobToRR({
          bodyshop,
          job,
          selectedCustomer: { customerNo: finalEffectiveCustNo, custNo: finalEffectiveCustNo },
          advisorNo: String(finalAdvisorNo),
          txEnvelope,
          socket,
          svId: ensured?.svId || null
        });
      }

      // Cache raw export result + pending RO number for finalize
      await redisHelpers.setSessionTransactionData(
        socket.id,
        ns,
        RRCacheEnums.ExportResult,
        result || {},
        defaultRRTTL
      );

      if (result?.success) {
        const data = result?.data || {};

        // Prefer explicit return from export function; then fall back to fields
        const dmsRoNo = result?.roNo ?? data?.dmsRoNo ?? null;

        const outsdRoNo = data?.outsdRoNo ?? job?.ro_number ?? job?.id ?? null;

        // ✅ Persist DMS RO number on the job (jobs.dms_id)
        if (dmsRoNo) {
          await setJobDmsIdForSocket({ socket, jobId: rid, dmsId: dmsRoNo });
        } else {
          CreateRRLogEvent(
            socket,
            "WARN",
            "RR export succeeded but no DMS RO number was returned",
            withRRRequestXml(result, {
              jobId: rid,
              resultPreview: {
                roNo: result?.roNo,
                data: {
                  dmsRoNo: data?.dmsRoNo,
                  outsdRoNo: data?.outsdRoNo
                }
              }
            })
          );
        }

        await redisHelpers.setSessionTransactionData(
          socket.id,
          ns,
          RRCacheEnums.PendingRO,
          {
            outsdRoNo,
            dmsRoNo,
            customerNo: String(effectiveCustNo),
            advisorNo: String(advisorNo),
            vin: job?.v_vin || null,
            rrPreview: result?.rrPreview || null
          },
          defaultRRTTL
        );

        CreateRRLogEvent(
          socket,
          "INFO",
          `{5} RO created. Waiting for validation.`,
          withRRRequestXml(result, {
            dmsRoNo: dmsRoNo || null,
            outsdRoNo: outsdRoNo || null
          })
        );

        // Tell FE to prompt for "Finished/Close"
        socket.emit("rr-validation-required", { jobId: rid, dmsRoNo, outsdRoNo });

        // Still emit info result if you want
        socket.emit("rr-export-job:result", { jobId: rid, bodyshopId: bodyshop?.id, result });

        // ACK but indicate it's pending finalize + include customer number + created flag
        ack?.({
          ok: true,
          pendingFinalize: true,
          dmsRoNo,
          outsdRoNo,
          result,
          custNo: String(effectiveCustNo),
          createdCustomer
        });
      } else {
        // classify & fail (no finalize)
        const tx = result?.statusBlocks?.transaction;

        const vendorStatusCode = Number(
          result?.roStatus?.statusCode ?? result?.roStatus?.StatusCode ?? tx?.statusCode ?? tx?.StatusCode
        );

        const vendorMessage =
          result?.roStatus?.message ??
          result?.roStatus?.Message ??
          tx?.message ??
          tx?.Message ??
          result?.error ??
          "RR export failed";

        const cls = classifyRRVendorError({
          code: vendorStatusCode,
          message: vendorMessage
        });

        CreateRRLogEvent(
          socket,
          "ERROR",
          `Export failed (step 1)`,
          withRRRequestXml(result, {
            roStatus: result?.roStatus,
            statusBlocks: result?.statusBlocks,
            classification: cls
          })
        );

        await insertRRFailedExportLog({
          socket,
          jobId: rid,
          job,
          bodyshop,
          error: new Error(cls.friendlyMessage || result?.error || "RR export failed"),
          classification: cls,
          result
        });

        socket.emit("export-failed", {
          ...withRRXmlSocketPayload(result, {
            vendor: "rr",
            jobId: rid,
            error: cls?.friendlyMessage || result?.error || "RR export failed"
          }),
          ...cls
        });

        ack?.({
          ok: false,
          error: cls.friendlyMessage || result?.error || "RR export failed",
          result,
          classification: cls
        });
      }
    } catch (error) {
      const cls = classifyRRVendorError(error);

      CreateRRLogEvent(
        socket,
        "ERROR",
        `Error during RR export (selected-customer)`,
        withRRRequestXml(error, {
          error: error.message,
          vendorStatusCode: cls.vendorStatusCode,
          code: cls.errorCode,
          friendly: cls.friendlyMessage,
          stack: error.stack,
          jobid: rid
        })
      );

      try {
        if (!bodyshop || !job) {
          const { bodyshopId } = await getSessionOrSocket(redisHelpers, socket);
          bodyshop = bodyshop || (await getBodyshopForSocket({ bodyshopId, socket }));
          job =
            job ||
            (await redisHelpers.getSessionTransactionData(socket.id, getTransactionType(rid), RRCacheEnums.JobData));
        }
      } catch {
        //
      }

      await insertRRFailedExportLog({
        socket,
        jobId: rid,
        job,
        bodyshop,
        error,
        classification: cls
      });

      try {
        socket.emit("export-failed", {
          ...withRRXmlSocketPayload(error, {
            vendor: "rr",
            jobId: rid,
            error: error.message
          }),
          ...cls
        });
        socket.emit("rr-user-notice", { jobId: rid, ...cls });
      } catch {
        //
      }

      ack?.({ ok: false, error: cls.friendlyMessage || error.message, classification: cls });
    }
  });

  socket.on("rr-finalize-repair-order", async ({ jobid, jobId } = {}, ack) => {
    const rid = resolveJobId(jobid || jobId, { jobid, jobId }, null);
    let bodyshop = null;
    let job = null;

    try {
      if (!rid) throw new Error("jobid required for finalize");

      const ns = getTransactionType(rid);
      const { bodyshopId } = await getSessionOrSocket(redisHelpers, socket);
      bodyshop = await getBodyshopForSocket({ bodyshopId, socket });

      job = await redisHelpers.getSessionTransactionData(socket.id, ns, RRCacheEnums.JobData);
      if (!job) job = await QueryJobData({ redisHelpers }, rid);

      const pending = await redisHelpers.getSessionTransactionData(socket.id, ns, RRCacheEnums.PendingRO);
      const advisorNo = await redisHelpers.getSessionTransactionData(socket.id, ns, RRCacheEnums.AdvisorNo);
      const selectedCustomerNo = await redisHelpers.getSessionTransactionData(
        socket.id,
        ns,
        RRCacheEnums.SelectedCustomer
      );

      if (!advisorNo) throw new Error("Advisor missing in session");
      if (!selectedCustomerNo) throw new Error("Customer number missing in session");

      // Prefer cached outsdRoNo; fall back to our deterministic external number
      const outsdRoNo = pending?.outsdRoNo ?? job?.ro_number ?? job?.id ?? null;
      // Prefer DMS RO for update, but finalize() will safely fall back to Outsd if missing
      const dmsRoNo = pending?.dmsRoNo ?? pending?.roNo ?? null;

      CreateRRLogEvent(socket, "DEBUG", `{6} Finalizing RR RO`, {
        jobId: rid,
        outsdRoNo,
        dmsRoNo,
        advisorNo,
        customerNo: selectedCustomerNo
      });

      const finalizeResult = await finalizeRRRepairOrder({
        bodyshop,
        job,
        advisorNo: String(advisorNo),
        customerNo: String(selectedCustomerNo),
        roNo: dmsRoNo, // ✅ RR requires roNo; finalize() will fall back to outsdRoNo if this is absent
        vin: pending?.vin,
        socket
      });

      if (finalizeResult?.success) {
        CreateRRLogEvent(
          socket,
          "INFO",
          `{7} Finalize success; marking exported`,
          withRRRequestXml(finalizeResult, { dmsRoNo, outsdRoNo })
        );

        // ✅ Mark exported + success log
        await markRRExportSuccess({
          socket,
          jobId: rid,
          job,
          bodyshop,
          result: finalizeResult,
          metaExtra: {
            rrPreview: pending?.rrPreview || null
          }
        });

        // Clean pending key
        try {
          await redisHelpers.setSessionTransactionData(socket.id, ns, RRCacheEnums.PendingRO, null, 1);
        } catch {
          //
        }

        socket.emit("export-success", { vendor: "rr", jobId: rid, roStatus: finalizeResult?.roStatus });
        ack?.({ ok: true, result: finalizeResult });
      } else {
        const tx = finalizeResult?.statusBlocks?.transaction;

        const vendorStatusCode = Number(
          finalizeResult?.roStatus?.statusCode ??
            finalizeResult?.roStatus?.StatusCode ??
            tx?.statusCode ??
            tx?.StatusCode
        );

        const vendorMessage =
          finalizeResult?.roStatus?.message ??
          finalizeResult?.roStatus?.Message ??
          tx?.message ??
          tx?.Message ??
          finalizeResult?.error ??
          "RR finalize failed";

        const cls = classifyRRVendorError({
          code: vendorStatusCode,
          message: vendorMessage
        });

        CreateRRLogEvent(
          socket,
          "ERROR",
          "Finalize failed",
          withRRRequestXml(finalizeResult, {
            roStatus: finalizeResult?.roStatus,
            statusBlocks: finalizeResult?.statusBlocks,
            classification: cls
          })
        );

        await insertRRFailedExportLog({
          socket,
          jobId: rid,
          job,
          bodyshop,
          error: new Error(cls.friendlyMessage || finalizeResult?.error || "RR finalize failed"),
          classification: cls,
          result: finalizeResult
        });

        socket.emit("export-failed", {
          ...withRRXmlSocketPayload(finalizeResult, {
            vendor: "rr",
            jobId: rid,
            error: cls?.friendlyMessage || finalizeResult?.error || "RR finalize failed"
          }),
          ...cls
        });
        ack?.({ ok: false, error: cls.friendlyMessage || "RR finalize failed", classification: cls });
      }
    } catch (error) {
      const cls = classifyRRVendorError(error);
      CreateRRLogEvent(
        socket,
        "ERROR",
        `Error during RR finalize`,
        withRRRequestXml(error, {
          error: error.message,
          vendorStatusCode: cls.vendorStatusCode,
          code: cls.errorCode,
          friendly: cls.friendlyMessage,
          stack: error.stack,
          jobid: rid
        })
      );

      try {
        if (!bodyshop || !job) {
          const { bodyshopId } = await getSessionOrSocket(redisHelpers, socket);
          bodyshop = bodyshop || (await getBodyshopForSocket({ bodyshopId, socket }));
          job =
            job ||
            (await redisHelpers.getSessionTransactionData(socket.id, getTransactionType(rid), RRCacheEnums.JobData));
        }
      } catch {
        //
      }

      await insertRRFailedExportLog({
        socket,
        jobId: rid,
        job,
        bodyshop,
        error,
        classification: cls
      });

      try {
        socket.emit(
          "export-failed",
          {
            ...withRRXmlSocketPayload(error, {
              vendor: "rr",
              jobId: rid,
              error: error.message
            }),
            ...cls
          }
        );
      } catch {
        //
      }

      ack?.({ ok: false, error: cls.friendlyMessage || error.message, classification: cls });
    }
  });

  // RR allocations preview (RR-only)
  // Accepts either:
  //  - legacy: (jobid, cb)
  //  - new: ({ jobId, opCode, opPrefix, opBase, opSuffix }, cb)
  socket.on("rr-calculate-allocations", async (payload, cb) => {
    // Normalize arguments
    const isObjectPayload = payload && typeof payload === "object";
    const jobid = isObjectPayload ? payload.jobId || payload.jobid || payload.id : payload;

    const opCodeFromClient =
      isObjectPayload &&
      (payload.opCode ||
        payload.opcode ||
        payload.op_code ||
        (payload.opPrefix || payload.opBase || payload.opSuffix
          ? `${payload.opPrefix || ""}${payload.opBase || ""}${payload.opSuffix || ""}`.trim()
          : null));

    try {
      CreateRRLogEvent(socket, "DEBUG", "rr-calculate-allocations: begin", { jobid, opCodeFromClient });

      const raw = await RRCalculateAllocations(socket, jobid);

      // If the helper returns an explicit error shape, just pass it through.
      if (raw && raw.ok === false) {
        cb?.(raw);
        socket.emit("rr-calculate-allocations:result", raw);
        CreateRRLogEvent(socket, "DEBUG", "rr-calculate-allocations: helper returned error", {
          jobid,
          error: raw.error
        });
        return;
      }

      let ack;
      let jobAllocations;

      if (Array.isArray(raw)) {
        // Legacy shape: plain allocations array
        jobAllocations = raw;
        ack = { jobAllocations: raw };
      } else {
        ack = raw || {};
        jobAllocations = Array.isArray(ack.jobAllocations) ? ack.jobAllocations : [];
      }

      // Start with client-supplied OpCode (if any); fall back to defaults.
      let opCode = opCodeFromClient || null;

      try {
        const { bodyshopId } = await getSessionOrSocket(redisHelpers, socket);
        const bodyshop = await getBodyshopForSocket({ bodyshopId, socket });

        // resolveRROpCodeFromBodyshop(bodyshop, existingOverride?)
        opCode = resolveRROpCodeFromBodyshop(bodyshop, opCode);

        CreateRRLogEvent(socket, "DEBUG", "rr-calculate-allocations: resolved OpCode", {
          opCode,
          opCodeFromClient
        });
      } catch (e) {
        CreateRRLogEvent(socket, "WARN", "rr-calculate-allocations: bodyshop lookup failed, using existing OpCode", {
          error: e.message,
          opCodeFromClient
        });
      }

      let rogg = null;
      let rolabor = null;

      try {
        rogg = buildRogogFromAllocations(jobAllocations, { opCode, payType: "Cust" });
        if (rogg) {
          rolabor = buildRolaborFromRogog(rogg, { payType: "Cust" });
        }
      } catch (e) {
        CreateRRLogEvent(socket, "WARN", "rr-calculate-allocations: failed to build ROGOG/ROLABOR preview", {
          error: e.message
        });
      }

      const enriched = {
        ...ack,
        rogg,
        rolabor
      };

      cb?.(enriched);
      socket.emit("rr-calculate-allocations:result", enriched);
      CreateRRLogEvent(socket, "DEBUG", "rr-calculate-allocations: success", {
        jobid,
        jobAllocations: jobAllocations.length,
        hasRogg: !!rogg,
        hasRolabor: !!rolabor
      });
    } catch (e) {
      CreateRRLogEvent(socket, "ERROR", "rr-calculate-allocations: failed", { error: e.message, jobid });
      cb?.({ ok: false, error: e.message });
    }
  });
};

module.exports = registerRREvents;
