const Dinero = require("dinero.js");
const queries = require("../graphql-client/queries");
const logger = require("../utils/logger");

Dinero.globalRoundingMode = "HALF_EVEN";
Dinero.globalFormatRoundingMode = "HALF_EVEN";

const PAYOUT_METHODS = {
  hourly: "hourly",
  commission: "commission"
};

const CURRENCY_PRECISION = 2;
const HOURS_PRECISION = 5;

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

const normalizeNumericString = (value) => {
  if (typeof value === "string") {
    return value.trim();
  }

  if (typeof value === "number" && Number.isFinite(value)) {
    const asString = value.toString();

    if (!asString.toLowerCase().includes("e")) {
      return asString;
    }

    return value.toFixed(12).replace(/0+$/, "").replace(/\.$/, "");
  }

  return `${value ?? ""}`.trim();
};

const decimalToDinero = (value, errorMessage = "Invalid numeric value.") => {
  const normalizedValue = normalizeNumericString(value);
  const parsedValue = Number(normalizedValue);

  if (!Number.isFinite(parsedValue)) {
    throw new Error(errorMessage);
  }

  const isNegative = normalizedValue.startsWith("-");
  const unsignedValue = normalizedValue.replace(/^[+-]/, "");
  const [wholePart = "0", fractionPartRaw = ""] = unsignedValue.split(".");
  const wholeDigits = wholePart.replace(/\D/g, "") || "0";
  const fractionDigits = fractionPartRaw.replace(/\D/g, "");
  const amount = Number(`${wholeDigits}${fractionDigits}` || "0") * (isNegative ? -1 : 1);

  return Dinero({
    amount,
    precision: fractionDigits.length
  });
};

const roundValueWithDinero = (value, precision, errorMessage) =>
  decimalToDinero(value, errorMessage).convertPrecision(precision, Dinero.globalRoundingMode).toUnit();

const roundCurrency = (value, errorMessage = "Invalid currency value.") =>
  roundValueWithDinero(value, CURRENCY_PRECISION, errorMessage);

const roundHours = (value, errorMessage = "Invalid hours value.") => roundValueWithDinero(value, HOURS_PRECISION, errorMessage);

const normalizePayoutMethod = (value) =>
  value === PAYOUT_METHODS.commission ? PAYOUT_METHODS.commission : PAYOUT_METHODS.hourly;

const hasOwnValue = (obj, key) => Object.prototype.hasOwnProperty.call(obj || {}, key);

const getJobSaleRateField = (laborType) => `rate_${String(laborType || "").toLowerCase()}`;

const getTeamMemberLabel = (teamMember) => {
  const fullName = `${teamMember?.employee?.first_name || ""} ${teamMember?.employee?.last_name || ""}`.trim();
  return fullName || teamMember?.employee?.id || teamMember?.employeeid || "unknown employee";
};

const parseRequiredNumber = (value, errorMessage) => {
  const parsed = Number(value);

  if (!Number.isFinite(parsed)) {
    throw new Error(errorMessage);
  }

  return parsed;
};

const buildFallbackPayoutContext = ({ laborType, rate }) => ({
  payout_type: "legacy",
  payout_method: "legacy",
  cut_percent_applied: null,
  source_labor_rate: null,
  source_labor_type: laborType,
  effective_rate: roundCurrency(rate)
});

function BuildPayoutDetails(job, teamMember, laborType) {
  const payoutMethod = normalizePayoutMethod(teamMember?.payout_method);
  const teamMemberLabel = getTeamMemberLabel(teamMember);
  const sourceLaborRateField = getJobSaleRateField(laborType);

  if (payoutMethod === PAYOUT_METHODS.hourly && !hasOwnValue(teamMember?.labor_rates, laborType)) {
    throw new Error(`Missing hourly payout rate for ${teamMemberLabel} on labor type ${laborType}.`);
  }

  if (payoutMethod === PAYOUT_METHODS.commission && !hasOwnValue(teamMember?.commission_rates, laborType)) {
    throw new Error(`Missing commission percent for ${teamMemberLabel} on labor type ${laborType}.`);
  }

  if (payoutMethod === PAYOUT_METHODS.commission && !hasOwnValue(job, sourceLaborRateField)) {
    throw new Error(`Missing sale rate ${sourceLaborRateField} for labor type ${laborType}.`);
  }

  const hourlyRate =
    payoutMethod === PAYOUT_METHODS.hourly
      ? roundCurrency(
          parseRequiredNumber(
            teamMember?.labor_rates?.[laborType],
            `Invalid hourly payout rate for ${teamMemberLabel} on labor type ${laborType}.`
          )
        )
      : null;

  const commissionPercent =
    payoutMethod === PAYOUT_METHODS.commission
      ? roundCurrency(
          parseRequiredNumber(
            teamMember?.commission_rates?.[laborType],
            `Invalid commission percent for ${teamMemberLabel} on labor type ${laborType}.`
          )
        )
      : null;

  if (commissionPercent !== null && (commissionPercent < 0 || commissionPercent > 100)) {
    throw new Error(`Commission percent for ${teamMemberLabel} on labor type ${laborType} must be between 0 and 100.`);
  }

  const sourceLaborRate =
    payoutMethod === PAYOUT_METHODS.commission
      ? roundCurrency(
          parseRequiredNumber(job?.[sourceLaborRateField], `Invalid sale rate ${sourceLaborRateField} for labor type ${laborType}.`)
        )
      : null;

  const effectiveRate =
    payoutMethod === PAYOUT_METHODS.commission
      ? roundCurrency((sourceLaborRate * toNumber(commissionPercent)) / 100)
      : hourlyRate;

  return {
    effectiveRate,
    payoutContext: {
      payout_type: payoutMethod === PAYOUT_METHODS.commission ? "cut" : "hourly",
      payout_method: payoutMethod,
      cut_percent_applied: commissionPercent,
      source_labor_rate: sourceLaborRate,
      source_labor_type: laborType,
      effective_rate: effectiveRate
    }
  };
}

function BuildGeneratedPayoutContext({ baseContext, generatedBy, generatedFrom, taskName, usedTicketFallback }) {
  return {
    ...(baseContext || {}),
    generated_by: generatedBy,
    generated_at: new Date().toISOString(),
    generated_from: generatedFrom,
    task_name: taskName,
    used_ticket_fallback: Boolean(usedTicketFallback)
  };
}

function getAllKeys(...objects) {
  return [...new Set(objects.flatMap((obj) => (obj ? Object.keys(obj) : [])))];
}

function buildPayAllMemo({ deltaHours, hasExpected, hasClaimed, userEmail }) {
  if (!hasClaimed && deltaHours > 0) {
    return `Add unflagged hours. (${userEmail})`;
  }

  if (!hasExpected && deltaHours < 0) {
    return `Remove flagged hours per assignment. (${userEmail})`;
  }

  return `Adjust flagged hours per assignment. (${userEmail})`;
}

exports.payall = async function (req, res) {
  const { jobid } = req.body;
  logger.log("job-payroll-pay-all", "DEBUG", req.user.email, jobid, null);

  const BearerToken = req.BearerToken;
  const client = req.userGraphQLClient;

  try {
    const { jobs_by_pk: job } = await client
      .setHeaders({ Authorization: BearerToken })
      .request(queries.QUERY_JOB_PAYROLL_DATA, {
        id: jobid
      });

    const { employeeHash, assignmentHash } = CalculateExpectedHoursForJob(job);
    const ticketHash = CalculateTicketsHoursForJob(job);

    if (assignmentHash.unassigned > 0) {
      res.json({ success: false, error: "Not all hours have been assigned." });
      return;
    }

    const ticketsToInsert = [];
    const employeeIds = getAllKeys(employeeHash, ticketHash);

    employeeIds.forEach((employeeId) => {
      const expectedByLabor = employeeHash[employeeId] || {};
      const claimedByLabor = ticketHash[employeeId] || {};

      getAllKeys(expectedByLabor, claimedByLabor).forEach((laborType) => {
        const expected = expectedByLabor[laborType];
        const claimed = claimedByLabor[laborType];
        const deltaHours = roundHours((expected?.hours || 0) - (claimed?.hours || 0));

        if (deltaHours === 0) {
          return;
        }

        const effectiveRate = roundCurrency(expected?.rate ?? claimed?.rate);
        const payoutContext = BuildGeneratedPayoutContext({
          baseContext:
            expected?.payoutContext ||
            claimed?.payoutContext ||
            buildFallbackPayoutContext({ laborType, rate: effectiveRate }),
          generatedBy: req.user.email,
          generatedFrom: "payall",
          taskName: "Pay All",
          usedTicketFallback: !expected && Boolean(claimed)
        });

        ticketsToInsert.push({
          task_name: "Pay All",
          jobid: job.id,
          bodyshopid: job.bodyshop.id,
          employeeid: employeeId,
          productivehrs: deltaHours,
          rate: effectiveRate,
          ciecacode: laborType,
          cost_center: job.bodyshop.md_responsibility_centers.defaults.costs[laborType],
          flat_rate: true,
          created_by: req.user.email,
          payout_context: payoutContext,
          memo: buildPayAllMemo({
            deltaHours,
            hasExpected: Boolean(expected),
            hasClaimed: Boolean(claimed),
            userEmail: req.user.email
          })
        });
      });
    });

    const filteredTickets = ticketsToInsert.filter((ticket) => ticket.productivehrs !== 0);

    if (filteredTickets.length > 0) {
      await client.request(queries.INSERT_TIME_TICKETS, {
        timetickets: filteredTickets
      });
    }

    res.json(filteredTickets);
  } catch (error) {
    logger.log("job-payroll-labor-totals-error", "ERROR", req.user.email, jobid, {
      jobid,
      error: JSON.stringify(error)
    });
    res.status(400).json({ error: error.message });
  }
};

function CalculateExpectedHoursForJob(job, filterToLbrTypes) {
  const assignmentHash = { unassigned: 0 };
  const employeeHash = {}; // employeeid => Cieca labor type => { hours, rate, payoutContext }
  const laborTypeFilter = Array.isArray(filterToLbrTypes) ? filterToLbrTypes : null;

  job.joblines
    .filter((jobline) => {
      if (!laborTypeFilter) {
        return true;
      }

      const convertedLaborType = jobline.convertedtolbr ? jobline.convertedtolbr_data?.mod_lbr_ty : null;
      return laborTypeFilter.includes(jobline.mod_lbr_ty) || (convertedLaborType && laborTypeFilter.includes(convertedLaborType));
    })
    .forEach((jobline) => {
      const laborType = jobline.convertedtolbr ? jobline.convertedtolbr_data?.mod_lbr_ty || jobline.mod_lbr_ty : jobline.mod_lbr_ty;
      const laborHours = roundHours(
        toNumber(jobline.mod_lb_hrs) + (jobline.convertedtolbr ? toNumber(jobline.convertedtolbr_data?.mod_lb_hrs) : 0)
      );

      if (laborHours === 0) {
        return;
      }

      if (jobline.assigned_team === null) {
        assignmentHash.unassigned = roundHours(assignmentHash.unassigned + laborHours);
        return;
      }

      const theTeam = job.bodyshop.employee_teams.find((team) => team.id === jobline.assigned_team);

      if (!theTeam) {
        assignmentHash.unassigned = roundHours(assignmentHash.unassigned + laborHours);
        return;
      }

      assignmentHash[jobline.assigned_team] = roundHours((assignmentHash[jobline.assigned_team] || 0) + laborHours);

      theTeam.employee_team_members.forEach((teamMember) => {
        const employeeId = teamMember.employee.id;
        const { effectiveRate, payoutContext } = BuildPayoutDetails(job, teamMember, laborType);

        if (!employeeHash[employeeId]) {
          employeeHash[employeeId] = {};
        }

        if (!employeeHash[employeeId][laborType]) {
          employeeHash[employeeId][laborType] = {
            hours: 0,
            rate: effectiveRate,
            payoutContext
          };
        }

        const hoursOwed = roundHours((toNumber(teamMember.percentage) * laborHours) / 100);
        employeeHash[employeeId][laborType].hours = roundHours(employeeHash[employeeId][laborType].hours + hoursOwed);
        employeeHash[employeeId][laborType].rate = effectiveRate;
        employeeHash[employeeId][laborType].payoutContext = payoutContext;
      });
    });

  return { assignmentHash, employeeHash };
}

function CalculateTicketsHoursForJob(job) {
  const ticketHash = {}; // employeeid => Cieca labor type => { hours, rate, payoutContext }

  job.timetickets.forEach((ticket) => {
    if (!ticket?.employeeid || !ticket?.ciecacode) {
      return;
    }

    if (!ticketHash[ticket.employeeid]) {
      ticketHash[ticket.employeeid] = {};
    }

    if (!ticketHash[ticket.employeeid][ticket.ciecacode]) {
      ticketHash[ticket.employeeid][ticket.ciecacode] = {
        hours: 0,
        rate: roundCurrency(ticket.rate),
        payoutContext: ticket.payout_context || null
      };
    }

    ticketHash[ticket.employeeid][ticket.ciecacode].hours = roundHours(
      ticketHash[ticket.employeeid][ticket.ciecacode].hours + toNumber(ticket.productivehrs)
    );

    if (ticket.rate !== null && ticket.rate !== undefined) {
      ticketHash[ticket.employeeid][ticket.ciecacode].rate = roundCurrency(ticket.rate);
    }

    if (ticket.payout_context) {
      ticketHash[ticket.employeeid][ticket.ciecacode].payoutContext = ticket.payout_context;
    }
  });

  return ticketHash;
}

exports.BuildPayoutDetails = BuildPayoutDetails;
exports.CalculateExpectedHoursForJob = CalculateExpectedHoursForJob;
exports.CalculateTicketsHoursForJob = CalculateTicketsHoursForJob;
exports.RoundPayrollHours = roundHours;
