export interface LedgerBreakdownEntry {
  id?: string | null;
  transaction_type?: string | null;
  description?: string | null;
  debit?: number | null;
  credit?: number | null;
  date?: string | null;
  created_at?: string | null;
}

export interface AccountRow {
  type: string;
  label: string;
  charges: number;
  appliedPayments: number;
  openBalance: number;
  count: number;
}

export const TYPE_LABELS: Record<string, string> = {
  assessment: "Assessment",
  late_fee: "Late Fee",
  interest: "Interest",
  admin_fee: "Admin Fee",
  legal_fee: "Legal Fee",
  violation: "Violation Fine",
  bank_fee: "Bank Fee",
  special_assessment: "Special Assessment",
  other: "Other",
};

export const WATERFALL_ORDER = [
  "bank_fee",
  "interest",
  "late_fee",
  "legal_fee",
  "admin_fee",
  "violation",
  "special_assessment",
  "assessment",
  "other",
];

const PAYMENT_TYPES = new Set([
  "payment",
  "prepayment",
  "refund",
  "applied_deposit",
  "applied_prepayment",
  "reversed_payment",
]);

const ASSESSMENT_TYPES = new Set(["assessment", "special_assessment"]);

const normalizeType = (type?: string | null) => type?.trim().toLowerCase() || "other";

const formatTypeLabel = (type: string) =>
  TYPE_LABELS[type] || type.replace(/_/g, " ").replace(/\b\w/g, (char) => char.toUpperCase());

const compareText = (left?: string | null, right?: string | null) => (left || "").localeCompare(right || "");

const sortEntriesChronologically = (entries: LedgerBreakdownEntry[]) =>
  [...entries].sort((left, right) => {
    const byDate = compareText(left.date, right.date);
    if (byDate !== 0) return byDate;

    // Within the same date, process charges (debits) before payments (credits)
    const leftIsPayment = PAYMENT_TYPES.has(normalizeType(left.transaction_type)) || (Number(left.credit || 0) > 0 && Number(left.debit || 0) === 0);
    const rightIsPayment = PAYMENT_TYPES.has(normalizeType(right.transaction_type)) || (Number(right.credit || 0) > 0 && Number(right.debit || 0) === 0);
    if (leftIsPayment !== rightIsPayment) return leftIsPayment ? 1 : -1;

    const byCreatedAt = compareText(left.created_at, right.created_at);
    if (byCreatedAt !== 0) return byCreatedAt;

    return compareText(left.id, right.id);
  });

const getAllocationOrder = (encounteredTypes: string[]) => [
  ...WATERFALL_ORDER,
  ...encounteredTypes.filter((encounteredType) => !WATERFALL_ORDER.includes(encounteredType)),
];

function buildLedgerOpenBalanceState(entries: LedgerBreakdownEntry[]) {
  const rowsByType = new Map<string, AccountRow>();
  const openBuckets: Record<string, number> = {};
  const encounteredTypes: string[] = [];
  let totalPayments = 0;

  const ensureRow = (type: string) => {
    const existing = rowsByType.get(type);
    if (existing) return existing;

    const created: AccountRow = {
      type,
      label: formatTypeLabel(type),
      charges: 0,
      appliedPayments: 0,
      openBalance: 0,
      count: 0,
    };

    rowsByType.set(type, created);
    encounteredTypes.push(type);
    return created;
  };

  const applyCharge = (type: string, amount: number) => {
    if (amount <= 0) return;

    const row = ensureRow(type);
    row.charges += amount;
    row.openBalance += amount;
    row.count += 1;
    openBuckets[type] = (openBuckets[type] || 0) + amount;
  };

  const applyDirectCredit = (type: string, amount: number) => {
    if (amount <= 0) return;

    const row = ensureRow(type);
    row.openBalance -= amount;
    openBuckets[type] = (openBuckets[type] || 0) - amount;
  };

  const applyPayment = (amount: number): number => {
    let remaining = amount;

    for (const bucketType of getAllocationOrder(encounteredTypes)) {
      const openAmount = openBuckets[bucketType] || 0;
      if (remaining <= 0 || openAmount <= 0) continue;

      const applied = Math.min(remaining, openAmount);
      const row = ensureRow(bucketType);

      row.appliedPayments += applied;
      row.openBalance -= applied;
      openBuckets[bucketType] = openAmount - applied;
      remaining -= applied;
    }
    return remaining;
  };

  const reversePayment = (amount: number) => {
    let remaining = amount;
    const reverseAllocationOrder = [...getAllocationOrder(encounteredTypes)].reverse();

    for (const bucketType of reverseAllocationOrder) {
      const row = ensureRow(bucketType);
      const reopenable = Math.max(0, row.appliedPayments);
      if (remaining <= 0 || reopenable <= 0) continue;

      const reopened = Math.min(remaining, reopenable);
      row.appliedPayments -= reopened;
      row.openBalance += reopened;
      openBuckets[bucketType] = (openBuckets[bucketType] || 0) + reopened;
      remaining -= reopened;
    }
  };

  let unappliedCredit = 0;

  for (const entry of sortEntriesChronologically(entries)) {
    const type = normalizeType(entry.transaction_type);
    const debit = Number(entry.debit || 0);
    const credit = Number(entry.credit || 0);

    const treatAsPayment = PAYMENT_TYPES.has(type);

    if (treatAsPayment) {
      const netPayment = credit - debit;

      if (netPayment > 0) {
        totalPayments += netPayment;
        unappliedCredit += applyPayment(netPayment);
      } else if (netPayment < 0) {
        totalPayments += netPayment;
        reversePayment(Math.abs(netPayment));
      }

      continue;
    }

    applyCharge(type, debit);
    applyDirectCredit(type, credit);

    // After adding a charge, try to apply any unapplied credit from earlier payments
    if (unappliedCredit > 0 && debit > 0) {
      unappliedCredit = applyPayment(unappliedCredit);
    }
  }

  return { rowsByType, openBuckets, totalPayments };
}

export function calculateRemainingUnpaidAssessmentBalance(entries: LedgerBreakdownEntry[]) {
  const { openBuckets } = buildLedgerOpenBalanceState(entries);

  return Array.from(ASSESSMENT_TYPES).reduce(
    (sum, type) => sum + Math.max(0, openBuckets[type] || 0),
    0
  );
}

export interface WaterfallAllocation {
  type: string;
  label: string;
  applied: number;
  openBefore: number;
  openAfter: number;
}

/**
 * Given existing ledger entries and a payment amount, compute how the payment
 * would be allocated across charge types following the waterfall priority.
 */
export function computePaymentWaterfallAllocation(
  entries: LedgerBreakdownEntry[],
  paymentAmount: number
): { allocations: WaterfallAllocation[]; totalOpen: number; remaining: number } {
  const { openBuckets } = buildLedgerOpenBalanceState(entries);
  const encounteredTypes = Object.keys(openBuckets);
  const allocationOrder = getAllocationOrder(encounteredTypes);

  const totalOpen = Object.values(openBuckets).reduce((s, v) => s + Math.max(0, v), 0);
  let remaining = paymentAmount;
  const allocations: WaterfallAllocation[] = [];

  for (const bucketType of allocationOrder) {
    const openAmount = Math.max(0, openBuckets[bucketType] || 0);
    if (openAmount <= 0) continue;

    const applied = Math.min(remaining, openAmount);
    allocations.push({
      type: bucketType,
      label: formatTypeLabel(bucketType),
      applied,
      openBefore: openAmount,
      openAfter: openAmount - applied,
    });
    remaining -= applied;
    if (remaining <= 0) break;
  }

  return { allocations, totalOpen, remaining: Math.max(0, remaining) };
}

export function buildLedgerAccountBreakdown(entries: LedgerBreakdownEntry[]) {
  const { rowsByType, totalPayments } = buildLedgerOpenBalanceState(entries);

  const rows = Array.from(rowsByType.values()).sort(
    (left, right) => right.openBalance - left.openBalance || left.label.localeCompare(right.label)
  );

  const totals = rows.reduce(
    (accumulator, row) => ({
      charges: accumulator.charges + row.charges,
      appliedPayments: accumulator.appliedPayments + row.appliedPayments,
      openBalance: accumulator.openBalance + row.openBalance,
      count: accumulator.count + row.count,
    }),
    { charges: 0, appliedPayments: 0, openBalance: 0, count: 0 }
  );

  return { rows, totals, totalPayments };
}