const round2 = (value) => Math.round((Number(value) + Number.EPSILON) * 100) / 100;

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

function exception(code, message) {
  return { code, message };
}

function blocked(employee, exceptions, values = {}) {
  return {
    status: "blocked",
    employee_code: employee?.employee_code ?? "UNKNOWN",
    exceptions,
    ...values,
  };
}

function ruleMetadataExceptions(ruleSet) {
  if (!ruleSet || ruleSet.status !== "approved") {
    return [exception("RULE_NOT_APPROVED", "Select a rule set with approved status before calculating.")];
  }
  if (!ruleSet.source_url) {
    return [exception("RULE_SOURCE_MISSING", "The selected rule must retain an official source URL.")];
  }
  if (!ruleSet.effective_from || !ruleSet.effective_to) {
    return [exception("RULE_EFFECTIVE_DATE_MISSING", "The selected rule requires both effective dates.")];
  }
  if (!ruleSet.approved_by) {
    return [exception("RULE_APPROVER_MISSING", "The selected rule requires a training reviewer.")];
  }
  return [];
}

function evidenceExceptionCode(code) {
  if (code === "ALLOWANCE_MEAL") return "ALLOWANCE_EVIDENCE_REQUIRED";
  return `${code}_EVIDENCE_REQUIRED`;
}

function validMapping(mapping, expectedKind) {
  return mapping
    && mapping.kind === expectedKind
    && typeof mapping.taxable_pit === "boolean"
    && typeof mapping.sso_eligible === "boolean"
    && typeof mapping.evidence_required === "boolean"
    && ["regular", "irregular"].includes(mapping.regularity)
    && ["remaining-periods", "current-period-only", "none"].includes(mapping.annualization_method);
}

function payrollItems(monthlyInput) {
  const primary = [
    { code: "BASE", amount: monthlyInput.base_salary, evidence_id: monthlyInput.base_salary_evidence_id, expected_kind: "earning" },
    { code: "COMMISSION", amount: monthlyInput.commission, evidence_id: monthlyInput.commission_evidence_id, expected_kind: "earning" },
    { code: "OTHER_EARNING", amount: monthlyInput.other_earnings, evidence_id: monthlyInput.other_earnings_evidence_id, expected_kind: "earning" },
    { code: "ALLOWANCE_MEAL", amount: monthlyInput.allowance, evidence_id: monthlyInput.allowance_evidence_id, expected_kind: "earning" },
    { code: "OTHER_DEDUCTION", amount: monthlyInput.employee_deductions, evidence_id: monthlyInput.employee_deductions_evidence_id, expected_kind: "deduction" },
  ];
  const earningItems = (Array.isArray(monthlyInput.earning_items) ? monthlyInput.earning_items : [])
    .map((item) => ({ ...item, expected_kind: "earning" }));
  const deductionItems = (Array.isArray(monthlyInput.deduction_items) ? monthlyInput.deduction_items : [])
    .map((item) => ({ ...item, expected_kind: "deduction" }));
  return [...primary, ...earningItems, ...deductionItems]
    .map((item) => ({ ...item, amount: number(item.amount) }))
    .filter((item) => item.amount !== 0);
}

export function calculateAnnualTax(taxableAnnual, brackets) {
  const taxable = Math.max(0, number(taxableAnnual));
  if (!Array.isArray(brackets) || brackets.length === 0) return 0;

  const tax = [...brackets]
    .sort((left, right) => number(left.from) - number(right.from))
    .reduce((total, bracket) => {
      const lower = Math.max(0, number(bracket.from));
      const upper = bracket.to == null ? Number.POSITIVE_INFINITY : number(bracket.to);
      const band = Math.max(0, Math.min(taxable, upper) - lower);
      return total + band * Math.max(0, number(bracket.rate));
    }, 0);

  return round2(tax);
}

export function selectEffectiveRule(rules, date, province, employeeClass) {
  const onDate = String(date ?? "");
  const candidates = (Array.isArray(rules) ? rules : [])
    .filter((rule) => rule.status === "approved")
    .filter((rule) => rule.employee_class === employeeClass)
    .filter((rule) => rule.province_scope === province || rule.province_scope === "All")
    .filter((rule) => rule.effective_from <= onDate && rule.effective_to >= onDate)
    .sort((left, right) => {
      const specificity = Number(right.province_scope === province) - Number(left.province_scope === province);
      return specificity || right.effective_from.localeCompare(left.effective_from);
    });

  return candidates[0] ?? null;
}

export function calculateDraft(employee, monthlyInput = {}, ruleSet) {
  const metadataExceptions = ruleMetadataExceptions(ruleSet);
  if (metadataExceptions.length) return blocked(employee, metadataExceptions);

  if (employee?.class !== ruleSet.employee_class) {
    return blocked(employee, [exception("UNSUPPORTED_EMPLOYEE_CLASS", `Employee class ${employee?.class ?? "missing"} is not supported by ${ruleSet.id ?? "the selected rule"}.`)]);
  }
  if (ruleSet.province_scope !== "All" && employee?.province !== ruleSet.province_scope) {
    return blocked(employee, [exception("UNSUPPORTED_PROVINCE", `Employee province ${employee?.province ?? "missing"} is outside the selected rule scope.`)]);
  }
  if (monthlyInput.pay_date && (monthlyInput.pay_date < ruleSet.effective_from || monthlyInput.pay_date > ruleSet.effective_to)) {
    return blocked(employee, [exception("RULE_NOT_EFFECTIVE", "The pay date is outside the selected rule's effective period.")]);
  }
  const items = payrollItems(monthlyInput);
  const unmappedCodes = [...new Set(items.filter((item) => !ruleSet.code_mappings?.[item.code]).map((item) => item.code))];
  if (unmappedCodes.length) {
    return blocked(employee, [exception("MISSING_CODE_MAPPING", `Map these earning/deduction codes before calculating: ${unmappedCodes.join(", ")}.`)]);
  }

  const invalidCodes = [...new Set(items
    .filter((item) => !validMapping(ruleSet.code_mappings[item.code], item.expected_kind))
    .map((item) => item.code))];
  if (invalidCodes.length) {
    return blocked(employee, [exception("INVALID_CODE_MAPPING", `Correct the approved mapping metadata for: ${invalidCodes.join(", ")}.`)]);
  }

  const missingEvidence = items.find((item) => ruleSet.code_mappings[item.code].evidence_required && !item.evidence_id);
  if (missingEvidence) {
    return blocked(employee, [exception(evidenceExceptionCode(missingEvidence.code), `Attach an approved evidence reference before including ${missingEvidence.code}.`)]);
  }

  const earningItems = items.filter((item) => item.expected_kind === "earning");
  const deductionItems = items.filter((item) => item.expected_kind === "deduction");
  const employeeDeductions = deductionItems.reduce((total, item) => total + item.amount, 0);
  const grossPay = round2(earningItems.reduce((total, item) => total + item.amount, 0));

  const ssoEligiblePay = Math.max(0, earningItems.reduce((total, item) => {
    return total + (ruleSet.code_mappings[item.code].sso_eligible ? item.amount : 0);
  }, 0));
  const ssoEmployee = round2(Math.min(ssoEligiblePay, number(ruleSet.sso_wage_cap)) * number(ruleSet.sso_rate));

  const remainingPeriods = Math.max(1, Math.trunc(number(monthlyInput.remaining_periods) || 1));
  const ytdGrossEarnings = number(monthlyInput.ytd_gross_earnings);
  const ytdWithheld = number(monthlyInput.ytd_withheld);
  const fixedDeductions = number(ruleSet.annual_expense) + number(ruleSet.annual_allowance) + number(ruleSet.annual_sso_deduction);
  const annualizedAmount = (item) => {
    const mapping = ruleSet.code_mappings[item.code];
    if (!mapping.taxable_pit) return 0;
    if (mapping.annualization_method === "remaining-periods") return item.amount * remainingPeriods;
    if (mapping.annualization_method === "current-period-only") return item.amount;
    return 0;
  };
  const regularAnnualGross = ytdGrossEarnings + earningItems.reduce((total, item) => {
    return total + (ruleSet.code_mappings[item.code].regularity === "regular" ? annualizedAmount(item) : 0);
  }, 0);
  const taxableAnnualGross = ytdGrossEarnings + earningItems.reduce((total, item) => total + annualizedAmount(item), 0);
  const regularTaxableAnnual = Math.max(0, regularAnnualGross - fixedDeductions);
  const taxableAnnual = Math.max(0, taxableAnnualGross - fixedDeductions);
  const regularAnnualTax = calculateAnnualTax(regularTaxableAnnual, ruleSet.brackets);
  const estimatedAnnualTax = calculateAnnualTax(taxableAnnual, ruleSet.brackets);
  const incrementalIrregularWithholding = round2(Math.max(0, estimatedAnnualTax - regularAnnualTax));
  const regularPeriodWithholding = round2(Math.max(0, regularAnnualTax - ytdWithheld) / remainingPeriods);
  const estimatedWithholdingTax = round2(regularPeriodWithholding + incrementalIrregularWithholding);
  const totalDeductions = round2(ssoEmployee + estimatedWithholdingTax + employeeDeductions);
  const netPay = round2(grossPay - totalDeductions);

  const values = {
    gross_pay: grossPay,
    sso_employee: ssoEmployee,
    estimated_annual_tax: estimatedAnnualTax,
    incremental_irregular_withholding: incrementalIrregularWithholding,
    estimated_withholding_tax: estimatedWithholdingTax,
    employee_deductions: round2(employeeDeductions),
    total_deductions: totalDeductions,
    net_pay: netPay,
    rule_id: ruleSet.id ?? "UNNAMED-RULE",
    taxable_annual: round2(taxableAnnual),
  };
  if (netPay < 0) {
    return blocked(employee, [exception("NEGATIVE_NET_PAY", "Deductions exceed gross pay; review the input and obtain HR/Payroll approval.")], values);
  }

  return {
    status: "draft",
    employee_code: employee.employee_code,
    exceptions: [],
    ...values,
  };
}

export function reconcilePayroll(drafts) {
  const rows = Array.isArray(drafts) ? drafts : [];
  const calculated = rows.filter((draft) => draft.status === "draft");
  const blockedRows = rows.filter((draft) => draft.status === "blocked");
  const sum = (field) => round2(calculated.reduce((total, draft) => total + number(draft[field]), 0));
  const grossPay = sum("gross_pay");
  const totalDeductions = sum("total_deductions");
  const netPay = sum("net_pay");

  return {
    status: blockedRows.length ? "blocked" : "pass",
    draft_count: rows.length,
    calculated_count: calculated.length,
    blocked_count: blockedRows.length,
    gross_pay: grossPay,
    sso_employee: sum("sso_employee"),
    estimated_withholding_tax: sum("estimated_withholding_tax"),
    employee_deductions: sum("employee_deductions"),
    total_deductions: totalDeductions,
    net_pay: netPay,
    identity_delta: round2(grossPay - totalDeductions - netPay),
    blocked_employee_codes: blockedRows.map((draft) => draft.employee_code),
  };
}
