/**
 * THIS IS A COPY of CDKCalculateAllocations, modified to:
 *  - Only calculate allocations needed for Reynolds & RR exports
 *  - Keep sales broken down into buckets (parts, taxable / non-taxable parts, taxable labor, non-taxable labor, extras)
 *  - Add extra logging for easier debugging
 *
 * Original comments follow.
 */

const { GraphQLClient } = require("graphql-request");
const Dinero = require("dinero.js");
const _ = require("lodash");

const queries = require("../graphql-client/queries");
const CreateRRLogEvent = require("./rr-logger-event");
const InstanceManager = require("../utils/instanceMgr").default;

const { DiscountNotAlreadyCounted } = InstanceManager({
  imex: require("../job/job-totals"),
  rome: require("../job/job-totals-USA")
});

/**
 * ============================
 *  Helpers / Summarizers
 * ============================
 */

const summarizeMoney = (dinero) => {
  if (!dinero || typeof dinero.getAmount !== "function") return { cents: null };
  return { cents: dinero.getAmount() };
};

const summarizeTaxAllocations = (tax) =>
  Object.entries(tax || {}).map(([key, entry]) => ({
    key,
    sale: summarizeMoney(entry?.sale),
    cost: summarizeMoney(entry?.cost)
  }));

const summarizeAllocationsArray = (arr) =>
  (arr || []).map((a) => ({
    center: a.center || a.tax || null,
    tax: a.tax || null,
    sale: summarizeMoney(a.sale || a.totalSale || Dinero()),
    cost: summarizeMoney(a.cost)
  }));

const toFiniteNumber = (value) => {
  const parsed = Number.parseFloat(value);
  return Number.isFinite(parsed) ? parsed : 0;
};

/**
 * Internal per-center bucket shape for *sales*.
 * We keep separate buckets for RR so we can split
 * taxable vs non-taxable parts and labor lines later.
 */
function emptyCenterBucket() {
  const zero = Dinero();
  return {
    // Parts
    partsSale: zero, // total parts (taxable + non-taxable)
    partsTaxableSale: zero, // parts that should be taxed in RR
    partsNonTaxableSale: zero, // parts that should NOT be taxed in RR

    // Labor
    laborTaxableSale: zero, // labor that should be taxed in RR
    laborNonTaxableSale: zero, // labor that should NOT be taxed in RR
    laborTaxableHours: 0,
    laborNonTaxableHours: 0,

    // Extras (MAPA/MASH/towing/PAO/etc)
    extrasSale: zero, // total extras (taxable + non-taxable)
    extrasTaxableSale: zero,
    extrasNonTaxableSale: zero
  };
}

function ensureCenterBucket(hash, center) {
  if (!hash[center]) hash[center] = emptyCenterBucket();
  return hash[center];
}

/**
 * Thin logger wrapper: always uses CreateRRLogEvent,
 * with structured data passed via meta arg.
 */
function createDebugLogger(connectionData) {
  return (msg, meta, level = "DEBUG") => {
    const baseMsg = "rr-calculate-allocations " + msg;
    CreateRRLogEvent(connectionData, level, baseMsg, meta !== undefined ? meta : undefined);
  };
}

/**
 * Query job data for allocations.
 */
async function QueryJobData(connectionData, token, jobid) {
  CreateRRLogEvent(connectionData, "DEBUG", "Querying job data for allocations", { jobid });

  const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {});
  const result = await client.setHeaders({ Authorization: token }).request(queries.GET_CDK_ALLOCATIONS, { id: jobid });

  return result.jobs_by_pk;
}

/**
 * Build tax allocation object depending on environment (imex vs rome).
 * This matches the original logic, just split into its own helper.
 */
function buildTaxAllocations(bodyshop, job) {
  return InstanceManager({
    executeFunction: true,
    deubg: true,
    args: [],
    imex: () => ({
      state: {
        center: bodyshop.md_responsibility_centers.taxes.state.name,
        sale: Dinero(job.job_totals.totals.state_tax),
        cost: Dinero(),
        profitCenter: bodyshop.md_responsibility_centers.taxes.state,
        costCenter: bodyshop.md_responsibility_centers.taxes.state
      },
      federal: {
        center: bodyshop.md_responsibility_centers.taxes.federal.name,
        sale: Dinero(job.job_totals.totals.federal_tax),
        cost: Dinero(),
        profitCenter: bodyshop.md_responsibility_centers.taxes.federal,
        costCenter: bodyshop.md_responsibility_centers.taxes.federal
      }
    }),
    rome: () => ({
      tax_ty1: {
        center: bodyshop.md_responsibility_centers.taxes.tax_ty1.name,
        sale: Dinero(job.job_totals.totals.us_sales_tax_breakdown.ty1Tax),
        cost: Dinero(),
        profitCenter: bodyshop.md_responsibility_centers.taxes.tax_ty1,
        costCenter: bodyshop.md_responsibility_centers.taxes.tax_ty1
      },
      tax_ty2: {
        center: bodyshop.md_responsibility_centers.taxes.tax_ty2.name,
        sale: Dinero(job.job_totals.totals.us_sales_tax_breakdown.ty2Tax),
        cost: Dinero(),
        profitCenter: bodyshop.md_responsibility_centers.taxes.tax_ty2,
        costCenter: bodyshop.md_responsibility_centers.taxes.tax_ty2
      },
      tax_ty3: {
        center: bodyshop.md_responsibility_centers.taxes.tax_ty3.name,
        sale: Dinero(job.job_totals.totals.us_sales_tax_breakdown.ty3Tax),
        cost: Dinero(),
        profitCenter: bodyshop.md_responsibility_centers.taxes.tax_ty3,
        costCenter: bodyshop.md_responsibility_centers.taxes.tax_ty3
      },
      tax_ty4: {
        center: bodyshop.md_responsibility_centers.taxes.tax_ty4.name,
        sale: Dinero(job.job_totals.totals.us_sales_tax_breakdown.ty4Tax),
        cost: Dinero(),
        profitCenter: bodyshop.md_responsibility_centers.taxes.tax_ty4,
        costCenter: bodyshop.md_responsibility_centers.taxes.tax_ty4
      },
      tax_ty5: {
        center: bodyshop.md_responsibility_centers.taxes.tax_ty5.name,
        sale: Dinero(job.job_totals.totals.us_sales_tax_breakdown.ty5Tax),
        cost: Dinero(),
        profitCenter: bodyshop.md_responsibility_centers.taxes.tax_ty5,
        costCenter: bodyshop.md_responsibility_centers.taxes.tax_ty5
      }
    })
  });
}

/**
 * ============================
 *  Tax Context & Helpers
 * ============================
 */

/**
 * Build a small "tax context" object from the job + current instance.
 *
 * This centralises all of the "is this category taxable?" logic so that
 * the rest of the allocation code just asks simple yes/no questions.
 *
 * IMPORTANT: we are **not** calculating any tax **amounts** here – that is
 * still handled by job-costing. We only need to know if a given sale bucket
 * should be treated as taxable vs non-taxable for RR (CustTxblNTxblFlag).
 */
function buildTaxContext(job = {}) {
  const isImex = !!InstanceManager({ imex: true }); // Canada
  const isRome = !!InstanceManager({ rome: true }); // US

  const toNumber = (v) => (v == null ? 0 : Number(v) || 0);

  const federalTaxRate = toNumber(job.federal_tax_rate);
  const stateTaxRate = toNumber(job.state_tax_rate);
  const localTaxRate = toNumber(job.local_tax_rate);

  const hasFederalRate = federalTaxRate > 0;
  const hasState = stateTaxRate > 0;
  const hasLocal = localTaxRate > 0;

  // "hasFederal" kept for backwards compatibility / logging (Canada only)
  const hasFederal = isImex && hasFederalRate;

  // Canada: if ANY of federal / state / local > 0, treat the job as
  // "everything taxable by default", then let line-level flags override
  // for parts where applicable.
  const globalAllTaxCanada = isImex && (hasFederalRate || hasState || hasLocal);

  const hasAnySalesTax = hasFederalRate || hasState || hasLocal;

  // Parts tax rate map (PAA/PAC/…)
  let partTaxRates = job.part_tax_rates || job.parts_tax_rates || {};
  if (typeof partTaxRates === "string") {
    try {
      partTaxRates = JSON.parse(partTaxRates);
    } catch {
      partTaxRates = {};
    }
  }
  if (!partTaxRates || typeof partTaxRates !== "object") {
    partTaxRates = {};
  }

  const tax_lbr_rt = toNumber(job.tax_lbr_rt); // labour
  const tax_paint_mat_rt = toNumber(job.tax_paint_mat_rt || job.tax_paint_mt_rate); // MAPA
  const tax_shop_mat_rt = toNumber(job.tax_shop_mat_rt); // MASH
  const tax_tow_rt = toNumber(job.tax_tow_rt); // towing
  const tax_sub_rt = toNumber(job.tax_sub_rt); // sublet (rarely used directly)

  const hasAnyPartsWithTax = Object.values(partTaxRates).some(
    (entry) => entry && entry.prt_tax_in && toNumber(entry.prt_tax_rt) > 0
  );

  const hasAnyTax =
    hasAnySalesTax ||
    tax_lbr_rt > 0 ||
    tax_paint_mat_rt > 0 ||
    tax_shop_mat_rt > 0 ||
    tax_tow_rt > 0 ||
    tax_sub_rt > 0 ||
    hasAnyPartsWithTax;

  return {
    isImex,
    isRome,

    federalTaxRate,
    stateTaxRate,
    localTaxRate,

    hasFederal,
    hasState,
    hasLocal,
    hasAnySalesTax,
    globalAllTaxCanada,

    partTaxRates,
    tax_lbr_rt,
    tax_paint_mat_rt,
    tax_shop_mat_rt,
    tax_tow_rt,
    tax_sub_rt,
    hasAnyPartsWithTax,
    hasAnyTax
  };
}

/**
 * Resolve the "PA" / part-type code (PAA/PAC/…) from a job line.
 */
function resolvePartType(line = {}) {
  return line.part_type || line.partType || line.pa_code || line.pa || null;
}

/**
 * Decide if a *part* line is taxable vs non-taxable for RR.
 *
 * Rules:
 *   - Canada (IMEX):
 *       - If ANY of federal_tax_rate / state_tax_rate / local_tax_rate > 0
 *         => everything is taxable by default (globalAllTaxCanada),
 *         unless tax_part is explicitly false.
 *       - Otherwise, use part_tax_rates[part_type] (prt_tax_in && prt_tax_rt > 0),
 *         with tax_part as final override.
 *   - US (ROME):
 *       - Use part_tax_rates[part_type] (prt_tax_in && prt_tax_rt > 0),
 *         with tax_part as final override.
 *
 *   - line.tax_part is treated as the *final* check:
 *       - tax_part === false => always non-taxable.
 *       - tax_part === true  => always taxable, even if we have no table entry.
 */
function isPartTaxable(line = {}, taxCtx) {
  if (!taxCtx) return !!line.tax_part;

  const { globalAllTaxCanada, partTaxRates } = taxCtx;

  // Explicit per-line override to *not* tax.
  if (typeof line.tax_part === "boolean" && line.tax_part === false) {
    return false;
  }

  // Canada: any federal/state/local tax rate set => all parts taxable,
  // unless explicitly turned off above.
  if (globalAllTaxCanada) {
    return true;
  }

  let taxable = false;

  const partType = resolvePartType(line);
  if (partType && partTaxRates && partTaxRates[partType]) {
    const entry = partTaxRates[partType];
    const rate = Number(entry?.prt_tax_rt || 0);
    const indicator = !!entry?.prt_tax_in;
    taxable = indicator && rate > 0;
  }

  // tax_part === true is treated as "final yes" even if we didn't find
  // a matching part_tax_rate entry.
  if (typeof line.tax_part === "boolean" && line.tax_part === true) {
    taxable = true;
  }

  return taxable;
}

/**
 * Decide if *labour* for this job is taxable.
 *
 *   - Canada (IMEX):
 *       - If ANY of federal_tax_rate / state_tax_rate / local_tax_rate > 0
 *         (globalAllTaxCanada) => all labour is taxable.
 *       - Else if tax_lbr_rt > 0   => labour taxable.
 *       - Else                     => non-taxable.
 *   - US (ROME):
 *       - tax_lbr_rt > 0           => labour taxable, otherwise not.
 */
function isLaborTaxable(_line, taxCtx) {
  if (!taxCtx) return false;
  const { isImex, globalAllTaxCanada, tax_lbr_rt } = taxCtx;

  if (isImex && globalAllTaxCanada) return true;
  return tax_lbr_rt > 0;
}

/**
 * Taxability helpers for "extras" buckets.
 * These are all job-level decisions; there are no per-line flags for them
 * in the data we currently work with.
 *
 * Canada: if globalAllTaxCanada is true, we treat these as taxable.
 */
function isMapaTaxable(taxCtx) {
  if (!taxCtx) return false;
  const { isImex, globalAllTaxCanada, tax_paint_mat_rt } = taxCtx;
  if (isImex && globalAllTaxCanada) return true;
  return tax_paint_mat_rt > 0;
}

function isMashTaxable(taxCtx) {
  if (!taxCtx) return false;
  const { isImex, globalAllTaxCanada, tax_shop_mat_rt } = taxCtx;
  if (isImex && globalAllTaxCanada) return true;
  return tax_shop_mat_rt > 0;
}

function isTowTaxable(taxCtx) {
  if (!taxCtx) return false;
  const { isImex, globalAllTaxCanada, tax_tow_rt } = taxCtx;
  if (isImex && globalAllTaxCanada) return true;
  return tax_tow_rt > 0;
}

/**
 * Helper to push an "extra" (MAPA/MASH/towing/PAO/etc) amount into the
 * appropriate taxable / non-taxable buckets for a given center.
 */
function addExtras(bucket, dineroAmount, isTaxable) {
  if (!bucket || !dineroAmount || typeof dineroAmount.add !== "function") return;
  bucket.extrasSale = bucket.extrasSale.add(dineroAmount);
  if (isTaxable) {
    bucket.extrasTaxableSale = bucket.extrasTaxableSale.add(dineroAmount);
  } else {
    bucket.extrasNonTaxableSale = bucket.extrasNonTaxableSale.add(dineroAmount);
  }
}

/**
 * Build profitCenterHash from joblines (parts + labor) and detect MAPA/MASH presence.
 * Now stores *buckets* instead of a single Dinero per center.
 */
function buildProfitCenterHash(job, debugLog, taxContext) {
  let hasMapaLine = false;
  let hasMashLine = false;

  const profitCenterHash = job.joblines.reduce((acc, val) => {
    // MAPA line?
    if (val.db_ref === "936008") {
      if (!hasMapaLine) {
        debugLog("Detected existing MAPA line in joblines", {
          joblineId: val.id,
          db_ref: val.db_ref
        });
      }
      hasMapaLine = true;
    }

    // MASH line?
    if (val.db_ref === "936007") {
      if (!hasMashLine) {
        debugLog("Detected existing MASH line in joblines", {
          joblineId: val.id,
          db_ref: val.db_ref
        });
      }
      hasMashLine = true;
    }

    // Parts
    if (val.profitcenter_part) {
      const bucket = ensureCenterBucket(acc, val.profitcenter_part);

      let amount = Dinero({
        amount: Math.round(val.act_price * 100)
      }).multiply(val.part_qty || 1);

      const hasDiscount = (val.prt_dsmk_m && val.prt_dsmk_m !== 0) || (val.prt_dsmk_p && val.prt_dsmk_p !== 0);

      if (hasDiscount && DiscountNotAlreadyCounted(val, job.joblines)) {
        const discount = val.prt_dsmk_m
          ? Dinero({ amount: Math.round(val.prt_dsmk_m * 100) })
          : Dinero({
              amount: Math.round(val.act_price * 100)
            })
              .multiply(val.part_qty || 0)
              .percentage(Math.abs(val.prt_dsmk_p || 0))
              .multiply(val.prt_dsmk_p > 0 ? 1 : -1);

        amount = amount.add(discount);
      }

      const taxable = isPartTaxable(val, taxContext);

      if (taxable) {
        bucket.partsTaxableSale = bucket.partsTaxableSale.add(amount);
      } else {
        bucket.partsNonTaxableSale = bucket.partsNonTaxableSale.add(amount);
      }

      // Keep total parts for compatibility / convenience
      bucket.partsSale = bucket.partsSale.add(amount);
    }

    // Labor
    if (val.profitcenter_labor && val.mod_lbr_ty) {
      const bucket = ensureCenterBucket(acc, val.profitcenter_labor);

      const rateKey = `rate_${val.mod_lbr_ty.toLowerCase()}`;
      const rate = job[rateKey];
      const lineHours = toFiniteNumber(val.mod_lb_hrs);

      const laborAmount = Dinero({
        amount: Math.round(rate * 100)
      }).multiply(val.mod_lb_hrs);

      if (isLaborTaxable(val, taxContext)) {
        bucket.laborTaxableSale = bucket.laborTaxableSale.add(laborAmount);
        bucket.laborTaxableHours += lineHours;
      } else {
        bucket.laborNonTaxableSale = bucket.laborNonTaxableSale.add(laborAmount);
        bucket.laborNonTaxableHours += lineHours;
      }
    }

    return acc;
  }, {});

  debugLog("profitCenterHash after joblines", {
    hasMapaLine,
    hasMashLine,
    centers: Object.entries(profitCenterHash).map(([center, b]) => ({
      center,
      parts: summarizeMoney(b.partsSale),
      partsTaxable: summarizeMoney(b.partsTaxableSale),
      partsNonTaxable: summarizeMoney(b.partsNonTaxableSale),
      laborTaxable: summarizeMoney(b.laborTaxableSale),
      laborNonTaxable: summarizeMoney(b.laborNonTaxableSale),
      laborTaxableHours: b.laborTaxableHours,
      laborNonTaxableHours: b.laborNonTaxableHours,
      extras: summarizeMoney(b.extrasSale),
      extrasTaxable: summarizeMoney(b.extrasTaxableSale),
      extrasNonTaxable: summarizeMoney(b.extrasNonTaxableSale)
    }))
  });

  return { profitCenterHash, hasMapaLine, hasMashLine };
}

/**
 * Build costCenterHash from bills and timetickets.
 */
function buildCostCenterHash(job, selectedDmsAllocationConfig, disablebillwip, debugLog) {
  let costCenterHash = {};

  // 1) Bills -> costs
  debugLog("disablebillwip flag", { disablebillwip });

  if (!disablebillwip) {
    costCenterHash = job.bills.reduce((billAcc, bill) => {
      bill.billlines.forEach((line) => {
        const targetCenter = selectedDmsAllocationConfig.costs[line.cost_center];
        if (!targetCenter) return;

        if (!billAcc[targetCenter]) billAcc[targetCenter] = Dinero();

        const lineDinero = Dinero({
          amount: Math.round((line.actual_cost || 0) * 100)
        })
          .multiply(line.quantity)
          .multiply(bill.is_credit_memo ? -1 : 1);

        billAcc[targetCenter] = billAcc[targetCenter].add(lineDinero);
      });
      return billAcc;
    }, {});
  }

  debugLog("costCenterHash after bills (pre-timetickets)", {
    centers: Object.entries(costCenterHash || {}).map(([center, dinero]) => ({
      center,
      ...summarizeMoney(dinero)
    }))
  });

  // 2) Timetickets -> costs
  job.timetickets.forEach((ticket) => {
    const effectiveHours =
      ticket.employee && ticket.employee.flat_rate ? ticket.productivehrs || 0 : ticket.actualhrs || 0;

    const ticketTotal = Dinero({
      amount: Math.round(ticket.rate * effectiveHours * 100)
    });

    const targetCenter = selectedDmsAllocationConfig.costs[ticket.ciecacode];
    if (!targetCenter) return;

    if (!costCenterHash[targetCenter]) costCenterHash[targetCenter] = Dinero();
    costCenterHash[targetCenter] = costCenterHash[targetCenter].add(ticketTotal);
  });

  debugLog("costCenterHash after timetickets", {
    centers: Object.entries(costCenterHash || {}).map(([center, dinero]) => ({
      center,
      ...summarizeMoney(dinero)
    }))
  });

  return costCenterHash;
}

/**
 * Add manual MAPA / MASH sales where needed (into extrasSale bucket).
 */
function applyMapaMashManualLines({
  job,
  selectedDmsAllocationConfig,
  bodyshop,
  profitCenterHash,
  hasMapaLine,
  hasMashLine,
  debugLog,
  taxContext
}) {
  // MAPA (paint materials)
  if (!hasMapaLine && job.job_totals.rates.mapa.total.amount > 0) {
    const mapaAccountName = selectedDmsAllocationConfig.profits.MAPA;
    const mapaAccount = bodyshop.md_responsibility_centers.profits.find((c) => c.name === mapaAccountName);

    if (mapaAccount) {
      const amount = Dinero(job.job_totals.rates.mapa.total);
      const taxable = isMapaTaxable(taxContext);

      debugLog("Adding MAPA Line Manually", {
        mapaAccountName,
        amount: summarizeMoney(amount),
        taxable
      });

      const bucket = ensureCenterBucket(profitCenterHash, mapaAccountName);
      addExtras(bucket, amount, taxable);
    } else {
      debugLog("NO MAPA ACCOUNT FOUND!!", { mapaAccountName });
    }
  }

  // MASH (shop materials)
  if (!hasMashLine && job.job_totals.rates.mash.total.amount > 0) {
    const mashAccountName = selectedDmsAllocationConfig.profits.MASH;
    const mashAccount = bodyshop.md_responsibility_centers.profits.find((c) => c.name === mashAccountName);

    if (mashAccount) {
      const amount = Dinero(job.job_totals.rates.mash.total);
      const taxable = isMashTaxable(taxContext);

      debugLog("Adding MASH Line Manually", {
        mashAccountName,
        amount: summarizeMoney(amount),
        taxable
      });

      const bucket = ensureCenterBucket(profitCenterHash, mashAccountName);
      addExtras(bucket, amount, taxable);
    } else {
      debugLog("NO MASH ACCOUNT FOUND!!", { mashAccountName });
    }
  }

  return profitCenterHash;
}

/**
 * Apply materials costing (MAPA/MASH cost side) when configured.
 */
function applyMaterialsCosting({ job, bodyshop, selectedDmsAllocationConfig, costCenterHash, debugLog }) {
  const { cdk_configuration } = bodyshop || {};

  if (!cdk_configuration?.sendmaterialscosting) return costCenterHash;

  debugLog("sendmaterialscosting enabled", {
    sendmaterialscosting: cdk_configuration.sendmaterialscosting,
    use_paint_scale_data: job.bodyshop.use_paint_scale_data,
    mixdataLength: job.mixdata?.length || 0
  });

  const percent = cdk_configuration.sendmaterialscosting;

  // Paint Mat (MAPA)
  const mapaAccountName = selectedDmsAllocationConfig.costs.MAPA;
  const mapaAccount = bodyshop.md_responsibility_centers.costs.find((c) => c.name === mapaAccountName);

  if (mapaAccount) {
    if (!costCenterHash[mapaAccountName]) costCenterHash[mapaAccountName] = Dinero();

    if (job.bodyshop.use_paint_scale_data === true) {
      if (job.mixdata && job.mixdata.length > 0) {
        debugLog("Using mixdata for MAPA cost", {
          mapaAccountName,
          totalliquidcost: job.mixdata[0] && job.mixdata[0].totalliquidcost
        });

        costCenterHash[mapaAccountName] = costCenterHash[mapaAccountName].add(
          Dinero({
            amount: Math.round(((job.mixdata[0] && job.mixdata[0].totalliquidcost) || 0) * 100)
          })
        );
      } else {
        debugLog("Using percentage of MAPA total (no mixdata)", { mapaAccountName });

        costCenterHash[mapaAccountName] = costCenterHash[mapaAccountName].add(
          Dinero(job.job_totals.rates.mapa.total).percentage(percent)
        );
      }
    } else {
      debugLog("Using percentage of MAPA total (no paint scale data)", { mapaAccountName });

      costCenterHash[mapaAccountName] = costCenterHash[mapaAccountName].add(
        Dinero(job.job_totals.rates.mapa.total).percentage(percent)
      );
    }
  } else {
    debugLog("NO MAPA ACCOUNT FOUND (costs)!!", { mapaAccountName });
  }

  // Shop Mat (MASH)
  const mashAccountName = selectedDmsAllocationConfig.costs.MASH;
  const mashAccount = bodyshop.md_responsibility_centers.costs.find((c) => c.name === mashAccountName);

  if (mashAccount) {
    debugLog("Adding MASH material costing", { mashAccountName });

    if (!costCenterHash[mashAccountName]) costCenterHash[mashAccountName] = Dinero();
    costCenterHash[mashAccountName] = costCenterHash[mashAccountName].add(
      Dinero(job.job_totals.rates.mash.total).percentage(percent)
    );
  } else {
    debugLog("NO MASH ACCOUNT FOUND (costs)!!", { mashAccountName });
  }

  return costCenterHash;
}

/**
 * Apply non-tax extras (PVRT, towing, storage, PAO).
 * Extras go into the extrasSale bucket (split taxable / non-taxable).
 */
function applyExtras({
  job,
  bodyshop,
  selectedDmsAllocationConfig,
  profitCenterHash,
  taxAllocations,
  debugLog,
  taxContext
}) {
  const { ca_bc_pvrt } = job;

  // BC PVRT -> state tax
  if (ca_bc_pvrt) {
    debugLog("Adding PVRT to state tax allocation", { ca_bc_pvrt });

    taxAllocations.state.sale = taxAllocations.state.sale.add(Dinero({ amount: Math.round((ca_bc_pvrt || 0) * 100) }));
  }

  // Towing
  if (job.towing_payable && job.towing_payable !== 0) {
    const towAccountName = selectedDmsAllocationConfig.profits.TOW;
    const towAccount = bodyshop.md_responsibility_centers.profits.find((c) => c.name === towAccountName);

    if (towAccount) {
      const amount = Dinero({
        amount: Math.round((job.towing_payable || 0) * 100)
      });
      const taxable = isTowTaxable(taxContext);

      debugLog("Adding towing_payable to TOW account", {
        towAccountName,
        towing_payable: job.towing_payable,
        taxable
      });

      const bucket = ensureCenterBucket(profitCenterHash, towAccountName);
      addExtras(bucket, amount, taxable);
    } else {
      debugLog("NO TOW ACCOUNT FOUND!!", { towAccountName });
    }
  }

  // Storage (shares TOW account)
  if (job.storage_payable && job.storage_payable !== 0) {
    const storageAccountName = selectedDmsAllocationConfig.profits.TOW;
    const towAccount = bodyshop.md_responsibility_centers.profits.find((c) => c.name === storageAccountName);

    if (towAccount) {
      const amount = Dinero({
        amount: Math.round((job.storage_payable || 0) * 100)
      });
      const taxable = isTowTaxable(taxContext);

      debugLog("Adding storage_payable to TOW account", {
        storageAccountName,
        storage_payable: job.storage_payable,
        taxable
      });

      const bucket = ensureCenterBucket(profitCenterHash, storageAccountName);
      addExtras(bucket, amount, taxable);
    } else {
      debugLog("NO STORAGE/TOW ACCOUNT FOUND!!", { storageAccountName });
    }
  }

  // Bottom line adjustment -> PAO
  if (job.adjustment_bottom_line && job.adjustment_bottom_line !== 0) {
    const otherAccountName = selectedDmsAllocationConfig.profits.PAO;
    const otherAccount = bodyshop.md_responsibility_centers.profits.find((c) => c.name === otherAccountName);

    if (otherAccount) {
      const amount = Dinero({
        amount: Math.round((job.adjustment_bottom_line || 0) * 100)
      });
      const taxable = !!(taxContext && taxContext.hasAnyTax);

      debugLog("Adding adjustment_bottom_line to PAO", {
        otherAccountName,
        adjustment_bottom_line: job.adjustment_bottom_line,
        taxable
      });

      const bucket = ensureCenterBucket(profitCenterHash, otherAccountName);
      addExtras(bucket, amount, taxable);
    } else {
      debugLog("NO PAO ACCOUNT FOUND!!", { otherAccountName });
    }
  }

  return { profitCenterHash, taxAllocations };
}

/**
 * Apply Rome-specific profile adjustments (parts + rates).
 * These also feed into the *sales* buckets.
 */
function applyRomeProfileAdjustments({
  job,
  bodyshop,
  selectedDmsAllocationConfig,
  profitCenterHash,
  debugLog,
  connectionData,
  taxContext
}) {
  // Only relevant for Rome instances
  if (!InstanceManager({ rome: true })) return profitCenterHash;

  if (!selectedDmsAllocationConfig || !selectedDmsAllocationConfig.profits) {
    debugLog("ROME profile adjustments skipped (no selectedDmsAllocationConfig.profits)");
    return profitCenterHash;
  }

  const partsAdjustments = job?.job_totals?.parts?.adjustments || {};
  const rateMap = job?.job_totals?.rates || {};

  debugLog("ROME profile adjustments block entered", {
    partAdjustmentKeys: Object.keys(partsAdjustments),
    rateKeys: Object.keys(rateMap)
  });

  const extrasTaxable = !!(taxContext && taxContext.hasAnyTax);

  // Parts adjustments
  Object.keys(partsAdjustments).forEach((key) => {
    const accountName = selectedDmsAllocationConfig.profits[key];
    const otherAccount = bodyshop.md_responsibility_centers.profits.find((c) => c.name === accountName);

    if (otherAccount) {
      const bucket = ensureCenterBucket(profitCenterHash, accountName);

      const adjMoney = Dinero(partsAdjustments[key]);
      addExtras(bucket, adjMoney, extrasTaxable);

      debugLog("Added parts adjustment", {
        key,
        accountName,
        adjustment: summarizeMoney(adjMoney),
        taxable: extrasTaxable
      });
    } else {
      CreateRRLogEvent(
        connectionData,
        "ERROR",
        "Error encountered in rr-calculate-allocations. Unable to find parts adjustment account.",
        { accountName, key }
      );
      debugLog("Missing parts adjustment account", { key, accountName });
    }
  });

  // Labor / materials adjustments (match CDK semantics: check `adjustment`, add `adjustments`)
  Object.keys(rateMap).forEach((key) => {
    const rate = rateMap[key];
    if (!rate || !rate.adjustment) return;

    const checkMoney = Dinero(rate.adjustment);
    if (checkMoney.isZero()) return;

    const accountName = selectedDmsAllocationConfig.profits[key.toUpperCase()];
    const otherAccount = bodyshop.md_responsibility_centers.profits.find((c) => c.name === accountName);

    if (otherAccount) {
      const bucket = ensureCenterBucket(profitCenterHash, accountName);

      // Note: we intentionally use `rate.adjustments` here to mirror CDK behaviour
      const adjMoney = Dinero(rate.adjustments);
      addExtras(bucket, adjMoney, extrasTaxable);

      debugLog("Added rate adjustment", {
        key,
        accountName,
        adjustment: summarizeMoney(adjMoney),
        taxable: extrasTaxable
      });
    } else {
      CreateRRLogEvent(
        connectionData,
        "ERROR",
        "Error encountered in rr-calculate-allocations. Unable to find rate adjustment account.",
        { accountName, key }
      );
      debugLog("Missing rate adjustment account", { key, accountName });
    }
  });

  return profitCenterHash;
}

/**
 * Build job-level profit/cost allocations for each center.
 * PUBLIC SHAPE (for RR):
 *   {
 *     center,
 *     partsSale,
 *     partsTaxableSale,
 *     partsNonTaxableSale,
 *     laborTaxableSale,
 *     laborNonTaxableSale,
 *     extrasSale,
 *     extrasTaxableSale,
 *     extrasNonTaxableSale,
 *     totalSale,
 *     cost,
 *     profitCenter,
 *     costCenter
 *   }
 */
function buildJobAllocations(bodyshop, profitCenterHash, costCenterHash, debugLog) {
  const centers = _.union(Object.keys(profitCenterHash), Object.keys(costCenterHash));

  const jobAllocations = centers.map((center) => {
    const bucket = profitCenterHash[center] || emptyCenterBucket();

    const extrasSale = bucket.extrasSale;
    const totalSale = bucket.partsSale.add(bucket.laborTaxableSale).add(bucket.laborNonTaxableSale).add(extrasSale);

    const profitCenter = bodyshop.md_responsibility_centers.profits.find((c) => c.name === center);
    const costCenter = bodyshop.md_responsibility_centers.costs.find((c) => c.name === center);

    return {
      center,

      // Parts
      partsSale: bucket.partsSale,
      partsTaxableSale: bucket.partsTaxableSale,
      partsNonTaxableSale: bucket.partsNonTaxableSale,

      // Labor
      laborTaxableSale: bucket.laborTaxableSale,
      laborNonTaxableSale: bucket.laborNonTaxableSale,
      laborTaxableHours: bucket.laborTaxableHours,
      laborNonTaxableHours: bucket.laborNonTaxableHours,

      // Extras
      extrasSale,
      extrasTaxableSale: bucket.extrasTaxableSale,
      extrasNonTaxableSale: bucket.extrasNonTaxableSale,

      totalSale,

      cost: costCenterHash[center] || Dinero(),

      profitCenter,
      costCenter
    };
  });

  debugLog(
    "jobAllocations built",
    jobAllocations.map((row) => ({
      center: row.center,
      parts: summarizeMoney(row.partsSale),
      partsTaxable: summarizeMoney(row.partsTaxableSale),
      partsNonTaxable: summarizeMoney(row.partsNonTaxableSale),
      laborTaxable: summarizeMoney(row.laborTaxableSale),
      laborNonTaxable: summarizeMoney(row.laborNonTaxableSale),
      extras: summarizeMoney(row.extrasSale),
      extrasTaxable: summarizeMoney(row.extrasTaxableSale),
      extrasNonTaxable: summarizeMoney(row.extrasNonTaxableSale),
      totalSale: summarizeMoney(row.totalSale),
      cost: summarizeMoney(row.cost)
    }))
  );

  return jobAllocations;
}

/**
 * Build tax allocations array from taxAllocations hash.
 * Shape is unchanged from original (except extra logging).
 */
function buildTaxAllocArray(taxAllocations, selectedDmsAllocationConfig, debugLog) {
  const taxAllocArray = Object.keys(taxAllocations)
    .filter((key) => taxAllocations[key].sale.getAmount() > 0 || taxAllocations[key].cost.getAmount() > 0)
    .map((key) => {
      if (
        key === "federal" &&
        selectedDmsAllocationConfig.gst_override &&
        selectedDmsAllocationConfig.gst_override !== ""
      ) {
        const ret = { ...taxAllocations[key], tax: key };
        ret.costCenter.dms_acctnumber = selectedDmsAllocationConfig.gst_override;
        ret.profitCenter.dms_acctnumber = selectedDmsAllocationConfig.gst_override;
        return ret;
      }
      return { ...taxAllocations[key], tax: key };
    });

  debugLog("taxAllocArray built", summarizeAllocationsArray(taxAllocArray));
  return taxAllocArray;
}

/**
 * Build adjustment allocations (ttl_adjustment + ttl_tax_adjustment).
 */
function buildAdjustmentAllocations(job, bodyshop, debugLog) {
  const ttlAdjArray = job.job_totals.totals.ttl_adjustment
    ? [
        {
          center: "SUB ADJ",
          sale: Dinero(job.job_totals.totals.ttl_adjustment),
          cost: Dinero(),
          profitCenter: {
            name: "SUB ADJ",
            accountdesc: "SUB ADJ",
            accountitem: "SUB ADJ",
            accountname: "SUB ADJ",
            dms_acctnumber: bodyshop.md_responsibility_centers.ttl_adjustment.dms_acctnumber
          },
          costCenter: {}
        }
      ]
    : [];

  const ttlTaxAdjArray = job.job_totals.totals.ttl_tax_adjustment
    ? [
        {
          center: "TAX ADJ",
          sale: Dinero(job.job_totals.totals.ttl_tax_adjustment),
          cost: Dinero(),
          profitCenter: {
            name: "TAX ADJ",
            accountdesc: "TAX ADJ",
            accountitem: "TAX ADJ",
            accountname: "TAX ADJ",
            dms_acctnumber: bodyshop.md_responsibility_centers.ttl_tax_adjustment.dms_acctnumber
          },
          costCenter: {}
        }
      ]
    : [];

  if (ttlAdjArray.length) {
    debugLog("ttl_adjustment allocation added", summarizeAllocationsArray(ttlAdjArray));
  }
  if (ttlTaxAdjArray.length) {
    debugLog("ttl_tax_adjustment allocation added", summarizeAllocationsArray(ttlTaxAdjArray));
  }

  return { ttlAdjArray, ttlTaxAdjArray };
}

/**
 * Core allocation calculation – RR-only, with bucketed sales.
 *
 * RETURN SHAPE:
 *   {
 *     jobAllocations,   // per-center buckets (see buildJobAllocations)
 *     taxAllocArray,    // tax allocations
 *     ttlAdjArray,      // ttl_adjustment allocations
 *     ttlTaxAdjArray    // ttl_tax_adjustment allocations
 *   }
 */
function calculateAllocations(connectionData, job) {
  const { bodyshop } = job;
  const debugLog = createDebugLogger(connectionData);

  debugLog("ENTER", {
    bodyshopId: bodyshop?.id,
    bodyshopName: bodyshop?.name,
    dms_allocation: job.dms_allocation,
    hasBills: Array.isArray(job.bills) ? job.bills.length : 0,
    joblines: Array.isArray(job.joblines) ? job.joblines.length : 0,
    timetickets: Array.isArray(job.timetickets) ? job.timetickets.length : 0
  });

  const taxContext = buildTaxContext(job);
  debugLog("Tax context initialised", {
    isImex: taxContext.isImex,
    isRome: taxContext.isRome,
    federalTaxRate: taxContext.federalTaxRate,
    stateTaxRate: taxContext.stateTaxRate,
    localTaxRate: taxContext.localTaxRate,
    hasFederal: taxContext.hasFederal,
    hasState: taxContext.hasState,
    hasLocal: taxContext.hasLocal,
    globalAllTaxCanada: taxContext.globalAllTaxCanada,
    tax_lbr_rt: taxContext.tax_lbr_rt,
    tax_paint_mat_rt: taxContext.tax_paint_mat_rt,
    tax_shop_mat_rt: taxContext.tax_shop_mat_rt,
    tax_tow_rt: taxContext.tax_tow_rt,
    hasAnyPartsWithTax: taxContext.hasAnyPartsWithTax,
    hasAnyTax: taxContext.hasAnyTax
  });

  // 1) Tax allocations
  let taxAllocations = buildTaxAllocations(bodyshop, job);
  debugLog("Initial taxAllocations", summarizeTaxAllocations(taxAllocations));

  // 2) Profit centers from job lines + MAPA/MASH detection
  const {
    profitCenterHash: initialProfitHash,
    hasMapaLine,
    hasMashLine
  } = buildProfitCenterHash(job, debugLog, taxContext);

  // 3) DMS allocation config
  const selectedDmsAllocationConfig =
    bodyshop.md_responsibility_centers.dms_defaults.find((d) => d.name === job.dms_allocation) || null;

  CreateRRLogEvent(connectionData, "DEBUG", "Using DMS Allocation for cost export", {
    allocationName: selectedDmsAllocationConfig && selectedDmsAllocationConfig.name
  });
  debugLog("Selected DMS allocation config", {
    name: selectedDmsAllocationConfig && selectedDmsAllocationConfig.name
  });

  // 4) Cost centers from bills and timetickets
  const disablebillwip = !!bodyshop?.pbs_configuration?.disablebillwip;
  let costCenterHash = buildCostCenterHash(job, selectedDmsAllocationConfig, disablebillwip, debugLog);

  // 5) Manual MAPA/MASH sales (when needed)
  let profitCenterHash = applyMapaMashManualLines({
    job,
    selectedDmsAllocationConfig,
    bodyshop,
    profitCenterHash: initialProfitHash,
    hasMapaLine,
    hasMashLine,
    debugLog,
    taxContext
  });

  // 6) Materials costing (MAPA/MASH cost side)
  costCenterHash = applyMaterialsCosting({
    job,
    bodyshop,
    selectedDmsAllocationConfig,
    costCenterHash,
    debugLog
  });

  // 7) PVRT / towing / storage / PAO extras
  ({ profitCenterHash, taxAllocations } = applyExtras({
    job,
    bodyshop,
    selectedDmsAllocationConfig,
    profitCenterHash,
    taxAllocations,
    debugLog,
    taxContext
  }));

  // 8) Rome-only profile-level adjustments
  profitCenterHash = applyRomeProfileAdjustments({
    job,
    bodyshop,
    selectedDmsAllocationConfig,
    profitCenterHash,
    debugLog,
    connectionData,
    taxContext
  });

  debugLog("profitCenterHash before jobAllocations build", {
    centers: Object.entries(profitCenterHash || {}).map(([center, b]) => ({
      center,
      parts: summarizeMoney(b.partsSale),
      partsTaxable: summarizeMoney(b.partsTaxableSale),
      partsNonTaxable: summarizeMoney(b.partsNonTaxableSale),
      laborTaxable: summarizeMoney(b.laborTaxableSale),
      laborNonTaxable: summarizeMoney(b.laborNonTaxableSale),
      extras: summarizeMoney(b.extrasSale),
      extrasTaxable: summarizeMoney(b.extrasTaxableSale),
      extrasNonTaxable: summarizeMoney(b.extrasNonTaxableSale)
    }))
  });
  debugLog("costCenterHash before jobAllocations build", {
    centers: Object.entries(costCenterHash || {}).map(([center, dinero]) => ({
      center,
      ...summarizeMoney(dinero)
    }))
  });

  // 9) Build job-level allocations & tax allocations
  const jobAllocations = buildJobAllocations(bodyshop, profitCenterHash, costCenterHash, debugLog);
  const taxAllocArray = buildTaxAllocArray(taxAllocations, selectedDmsAllocationConfig, debugLog);
  const { ttlAdjArray, ttlTaxAdjArray } = buildAdjustmentAllocations(job, bodyshop, debugLog);

  const result = {
    jobAllocations,
    taxAllocArray,
    ttlAdjArray,
    ttlTaxAdjArray
  };

  debugLog("FINAL allocations summary", {
    jobAllocationsCount: jobAllocations.length,
    taxAllocCount: taxAllocArray.length,
    ttlAdjCount: ttlAdjArray.length,
    ttlTaxAdjCount: ttlTaxAdjArray.length
  });
  debugLog("EXIT");

  return result;
}

/**
 * HTTP route wrapper (kept for compatibility; still logs via RR logger).
 * Responds with { data: { jobAllocations, taxAllocArray, ttlAdjArray, ttlTaxAdjArray } }
 */
exports.defaultRoute = async function (req, res) {
  try {
    CreateRRLogEvent(req, "DEBUG", "Received request to calculate allocations", { jobid: req.body.jobid });

    const jobData = await QueryJobData(req, req.BearerToken, req.body.jobid);
    const data = calculateAllocations(req, jobData);
    return res.status(200).json({ data });
  } catch (error) {
    CreateRRLogEvent(req, "ERROR", "Error encountered in rr-calculate-allocations.", {
      message: error?.message || String(error),
      stack: error?.stack
    });
    res.status(500).json({ error: `Error encountered in rr-calculate-allocations. ${error}` });
  }
};

/**
 * Socket entry point (what rr-job-export & rr-register-socket-events call).
 * Reynolds-only: WSS + RR logger.
 *
 * Returns the same object as calculateAllocations().
 */
exports.default = async function (socket, jobid) {
  try {
    const token = `Bearer ${socket.handshake.auth.token}`;
    const jobData = await QueryJobData(socket, token, jobid);
    return calculateAllocations(socket, jobData);
  } catch (error) {
    CreateRRLogEvent(socket, "ERROR", "Error encountered in rr-calculate-allocations.", {
      message: error?.message || String(error),
      stack: error?.stack
    });
    return null;
  }
};
