export type SplitMethod =
  | "EQUAL"
  | "OWNERSHIP"
  | "CUSTOM_PERCENTAGE"
  | "CUSTOM_AMOUNT"
  | "PERSONAL";

export type PartnerInput = {
  id: string;
  name: string;
  ownershipPercentage: number;
  active?: boolean;
};

export type ExpenseSplitInput = {
  finalCost: number;
  paidByPartnerId?: string | null;
  splitMethod: SplitMethod;
  splitBetweenPartnerIds?: string[];
  customPercentages?: Record<string, number>;
  customAmounts?: Record<string, number>;
  personalPartnerId?: string | null;
};

export type ExpenseShareResult = {
  partnerId: string;
  shareAmount: number;
  sharePercentage?: number;
};

export type BalanceInput = {
  paidByPartnerId?: string | null;
  handledByPartnerId?: string | null;
  finalCost: number;
  shares: ExpenseShareResult[];
  creditType?: "CAPITAL_CONTRIBUTION" | "OUT_OF_POCKET" | "REIMBURSEMENT" | "OTHER";
  projectFundPayment?: boolean;
};

export type PartnerBalance = {
  partnerId: string;
  name: string;
  ownershipPercentage: number;
  capitalContributed: number;
  outOfPocketPaid: number;
  projectFundPaymentsHandled: number;
  totalPaid: number;
  totalResponsibleShare: number;
  netBalance: number;
};

export type SettlementInstruction = {
  fromPartnerId: string;
  fromName: string;
  toPartnerId: string;
  toName: string;
  amount: number;
};

export function roundMoney(value: number) {
  return Math.round((value + Number.EPSILON) * 100) / 100;
}

function selectedPartners(partners: PartnerInput[], ids?: string[]) {
  const active = partners.filter((partner) => partner.active !== false);
  if (!ids || ids.length === 0) return active;
  const selected = active.filter((partner) => ids.includes(partner.id));
  return selected.length > 0 ? selected : active;
}

function allocateWithResidual(
  partnerIds: string[],
  rawAmounts: number[],
  percentages?: number[]
) {
  const rounded = rawAmounts.map(roundMoney);
  const total = roundMoney(rawAmounts.reduce((sum, amount) => sum + amount, 0));
  const residual = roundMoney(total - rounded.reduce((sum, amount) => sum + amount, 0));

  if (rounded.length > 0 && residual !== 0) {
    rounded[rounded.length - 1] = roundMoney(rounded[rounded.length - 1] + residual);
  }

  return partnerIds.map((partnerId, index) => ({
    partnerId,
    shareAmount: rounded[index] ?? 0,
    sharePercentage: percentages?.[index]
  }));
}

export function calculateExpenseShares(
  partners: PartnerInput[],
  input: ExpenseSplitInput
): ExpenseShareResult[] {
  const finalCost = roundMoney(input.finalCost);
  const selected = selectedPartners(partners, input.splitBetweenPartnerIds);

  if (finalCost === 0 || selected.length === 0) return [];

  if (input.splitMethod === "PERSONAL") {
    const partnerId = input.personalPartnerId ?? input.paidByPartnerId ?? selected[0].id;
    return [{ partnerId, shareAmount: finalCost, sharePercentage: 100 }];
  }

  if (input.splitMethod === "CUSTOM_AMOUNT") {
    const entries = selected.map((partner) => ({
      partnerId: partner.id,
      amount: roundMoney(input.customAmounts?.[partner.id] ?? 0)
    }));
    const total = roundMoney(entries.reduce((sum, entry) => sum + entry.amount, 0));
    if (Math.abs(total - finalCost) > 0.01) {
      throw new Error("Custom amounts must equal the final cost.");
    }
    return entries.map((entry) => ({
      partnerId: entry.partnerId,
      shareAmount: entry.amount,
      sharePercentage: finalCost === 0 ? 0 : roundMoney((entry.amount / finalCost) * 100)
    }));
  }

  if (input.splitMethod === "CUSTOM_PERCENTAGE") {
    const percentages = selected.map((partner) => input.customPercentages?.[partner.id] ?? 0);
    const totalPercentage = roundMoney(percentages.reduce((sum, value) => sum + value, 0));
    if (Math.abs(totalPercentage - 100) > 0.01) {
      throw new Error("Custom percentages must total 100%.");
    }
    return allocateWithResidual(
      selected.map((partner) => partner.id),
      percentages.map((percentage) => (finalCost * percentage) / 100),
      percentages
    );
  }

  if (input.splitMethod === "OWNERSHIP") {
    const ownershipTotal = selected.reduce(
      (sum, partner) => sum + Number(partner.ownershipPercentage || 0),
      0
    );
    if (ownershipTotal <= 0) {
      return calculateExpenseShares(partners, { ...input, splitMethod: "EQUAL" });
    }
    const percentages = selected.map((partner) =>
      roundMoney((Number(partner.ownershipPercentage || 0) / ownershipTotal) * 100)
    );
    return allocateWithResidual(
      selected.map((partner) => partner.id),
      selected.map((partner) => (finalCost * Number(partner.ownershipPercentage || 0)) / ownershipTotal),
      percentages
    );
  }

  const equalShare = finalCost / selected.length;
  const equalPercentage = roundMoney(100 / selected.length);
  return allocateWithResidual(
    selected.map((partner) => partner.id),
    selected.map(() => equalShare),
    selected.map(() => equalPercentage)
  );
}

export function calculatePartnerBalances(
  partners: PartnerInput[],
  expenses: BalanceInput[]
): PartnerBalance[] {
  return partners.map((partner) => {
    const creditedInputs = expenses.filter((expense) => expense.paidByPartnerId === partner.id);
    const capitalContributed = creditedInputs
      .filter((expense) => expense.creditType === "CAPITAL_CONTRIBUTION")
      .reduce((sum, expense) => sum + expense.finalCost, 0);
    const outOfPocketPaid = creditedInputs
      .filter((expense) => expense.creditType !== "CAPITAL_CONTRIBUTION")
      .reduce((sum, expense) => sum + expense.finalCost, 0);
    const projectFundPaymentsHandled = expenses
      .filter((expense) => expense.handledByPartnerId === partner.id && expense.projectFundPayment)
      .reduce((sum, expense) => sum + expense.finalCost, 0);
    const totalPaid = capitalContributed + outOfPocketPaid;
    const totalResponsibleShare = expenses
      .flatMap((expense) => expense.shares)
      .filter((share) => share.partnerId === partner.id)
      .reduce((sum, share) => sum + share.shareAmount, 0);

    return {
      partnerId: partner.id,
      name: partner.name,
      ownershipPercentage: Number(partner.ownershipPercentage || 0),
      capitalContributed: roundMoney(capitalContributed),
      outOfPocketPaid: roundMoney(outOfPocketPaid),
      projectFundPaymentsHandled: roundMoney(projectFundPaymentsHandled),
      totalPaid: roundMoney(totalPaid),
      totalResponsibleShare: roundMoney(totalResponsibleShare),
      netBalance: roundMoney(totalPaid - totalResponsibleShare)
    };
  });
}

export function createSettlementInstructions(
  balances: PartnerBalance[]
): SettlementInstruction[] {
  const debtors = balances
    .filter((balance) => balance.netBalance < -0.009)
    .map((balance) => ({ ...balance, remaining: roundMoney(Math.abs(balance.netBalance)) }))
    .sort((a, b) => b.remaining - a.remaining);
  const creditors = balances
    .filter((balance) => balance.netBalance > 0.009)
    .map((balance) => ({ ...balance, remaining: roundMoney(balance.netBalance) }))
    .sort((a, b) => b.remaining - a.remaining);

  const instructions: SettlementInstruction[] = [];
  let debtorIndex = 0;
  let creditorIndex = 0;

  while (debtorIndex < debtors.length && creditorIndex < creditors.length) {
    const debtor = debtors[debtorIndex];
    const creditor = creditors[creditorIndex];
    const amount = roundMoney(Math.min(debtor.remaining, creditor.remaining));

    if (amount > 0) {
      instructions.push({
        fromPartnerId: debtor.partnerId,
        fromName: debtor.name,
        toPartnerId: creditor.partnerId,
        toName: creditor.name,
        amount
      });
    }

    debtor.remaining = roundMoney(debtor.remaining - amount);
    creditor.remaining = roundMoney(creditor.remaining - amount);

    if (debtor.remaining <= 0.009) debtorIndex += 1;
    if (creditor.remaining <= 0.009) creditorIndex += 1;
  }

  return instructions;
}

export function calculateRentBalance(input: {
  rentExpected: number;
  rentPaid: number;
  lateFee?: number;
  utilityCharge?: number;
  parkingFee?: number;
  otherCharge?: number;
  month?: Date;
  rentDueDay?: number;
}) {
  const balanceDue = roundMoney(
    Number(input.rentExpected || 0) +
      Number(input.lateFee || 0) +
      Number(input.utilityCharge || 0) +
      Number(input.parkingFee || 0) +
      Number(input.otherCharge || 0) -
      Number(input.rentPaid || 0)
  );

  let status: "PAID" | "PARTIAL" | "DUE" | "LATE" = "DUE";
  if (balanceDue <= 0) {
    status = "PAID";
  } else if (Number(input.rentPaid || 0) > 0) {
    status = "PARTIAL";
  } else if (input.month) {
    const dueDate = new Date(input.month);
    dueDate.setDate(input.rentDueDay ?? 1);
    if (new Date() > dueDate) status = "LATE";
  }

  return { balanceDue, status };
}

export function calculateProfitLoss(input: {
  income: number[];
  expenses: number[];
  partners: PartnerInput[];
}) {
  const totalIncome = roundMoney(input.income.reduce((sum, amount) => sum + amount, 0));
  const totalExpenses = roundMoney(input.expenses.reduce((sum, amount) => sum + amount, 0));
  const netProfitLoss = roundMoney(totalIncome - totalExpenses);
  const ownershipTotal = input.partners.reduce(
    (sum, partner) => sum + Number(partner.ownershipPercentage || 0),
    0
  );

  const partnerShares = input.partners.map((partner) => {
    const percentage =
      ownershipTotal > 0 ? Number(partner.ownershipPercentage || 0) / ownershipTotal : 0;
    return {
      partnerId: partner.id,
      name: partner.name,
      amount: roundMoney(netProfitLoss * percentage)
    };
  });

  return { totalIncome, totalExpenses, netProfitLoss, partnerShares };
}
