import { CommentaryProto } from '../types.js';
import { DecoratedLedgerRecord } from './profit-and-loss.helper.js';

type EntityAmountAccumulator = {
  amount: number;
  // ledger records are keyed by ID, so a record hitting the same entity on multiple sides is listed once
  ledgerRecords: Map<string, DecoratedLedgerRecord>;
};

type AmountsBySortCode = Map<
  number,
  { amount: number; records: Map<string, EntityAmountAccumulator> }
>;

export function updateRecords(
  amountsByEntity: AmountsBySortCode,
  amount: number,
  sortCode: number,
  financialEntityId: string,
  ledgerRecord: DecoratedLedgerRecord,
) {
  const currentRecord = amountsByEntity.get(sortCode);
  if (currentRecord) {
    currentRecord.amount += amount;
    const record = currentRecord.records.get(financialEntityId);
    if (record) {
      record.amount += amount;
      record.ledgerRecords.set(ledgerRecord.id, ledgerRecord);
    } else {
      currentRecord.records.set(financialEntityId, {
        amount,
        ledgerRecords: new Map([[ledgerRecord.id, ledgerRecord]]),
      });
    }
  } else {
    amountsByEntity.set(sortCode, {
      amount,
      records: new Map([
        [financialEntityId, { amount, ledgerRecords: new Map([[ledgerRecord.id, ledgerRecord]]) }],
      ]),
    });
  }
}

export type FilteringRule = (financialEntityId: string, sortCode: number) => boolean;
export type FilteringOptions = {
  rule: FilteringRule;
  negate?: boolean;
};

export function amountByFinancialEntityIdAndSortCodeValidations<
  T extends FilteringOptions[],
  R extends { [K in keyof T]: number },
>(unfilteredRecords: DecoratedLedgerRecord[], filteringRules: T): R {
  const amounts = filteringRules.map(() => 0) as R;
  unfilteredRecords.map(record => {
    if (record.credit_entity1) {
      for (let i = 0; i < filteringRules.length; i++) {
        if (filteringRules[i].rule(record.credit_entity1, record.credit_entity_sort_code1!)) {
          amounts[i] += Number(record.credit_local_amount1) * (filteringRules[i].negate ? -1 : 1);
        }
      }
    }
    if (record.credit_entity2) {
      for (let i = 0; i < filteringRules.length; i++) {
        if (filteringRules[i].rule(record.credit_entity2, record.credit_entity_sort_code2!)) {
          amounts[i] += Number(record.credit_local_amount2) * (filteringRules[i].negate ? -1 : 1);
        }
      }
    }
    if (record.debit_entity1) {
      for (let i = 0; i < filteringRules.length; i++) {
        if (filteringRules[i].rule(record.debit_entity1, record.debit_entity_sort_code1!)) {
          amounts[i] += Number(record.debit_local_amount1) * (filteringRules[i].negate ? 1 : -1);
        }
      }
    }
    if (record.debit_entity2) {
      for (let i = 0; i < filteringRules.length; i++) {
        if (filteringRules[i].rule(record.debit_entity2, record.debit_entity_sort_code2!)) {
          amounts[i] += Number(record.debit_local_amount2) * (filteringRules[i].negate ? 1 : -1);
        }
      }
    }
  });

  return amounts;
}

export function recordsByFinancialEntityIdAndSortCodeValidations<
  T extends FilteringOptions[],
  R extends { [K in keyof T]: CommentaryProto },
>(unfilteredRecords: DecoratedLedgerRecord[], filteringRules: T): R {
  const commentaryProtos = filteringRules.map(() => ({
    amount: 0,
    amountsByEntity: new Map() as AmountsBySortCode,
  }));
  unfilteredRecords.map(record => {
    if (record.credit_entity1) {
      for (let i = 0; i < filteringRules.length; i++) {
        if (filteringRules[i].rule(record.credit_entity1, record.credit_entity_sort_code1!)) {
          const sign = filteringRules[i].negate ? -1 : 1;
          const recordAmount = Number(record.credit_local_amount1) * sign;
          updateRecords(
            commentaryProtos[i].amountsByEntity,
            recordAmount,
            record.credit_entity_sort_code1!,
            record.credit_entity1,
            record,
          );
          commentaryProtos[i].amount += recordAmount;
        }
      }
    }
    if (record.credit_entity2) {
      for (let i = 0; i < filteringRules.length; i++) {
        if (filteringRules[i].rule(record.credit_entity2, record.credit_entity_sort_code2!)) {
          const sign = filteringRules[i].negate ? -1 : 1;
          const recordAmount = Number(record.credit_local_amount2) * sign;
          updateRecords(
            commentaryProtos[i].amountsByEntity,
            recordAmount,
            record.credit_entity_sort_code2!,
            record.credit_entity2,
            record,
          );
          commentaryProtos[i].amount += recordAmount;
        }
      }
    }
    if (record.debit_entity1) {
      for (let i = 0; i < filteringRules.length; i++) {
        if (filteringRules[i].rule(record.debit_entity1, record.debit_entity_sort_code1!)) {
          const sign = filteringRules[i].negate ? -1 : 1;
          const recordAmount = Number(record.debit_local_amount1) * sign * -1;
          updateRecords(
            commentaryProtos[i].amountsByEntity,
            recordAmount,
            record.debit_entity_sort_code1!,
            record.debit_entity1,
            record,
          );
          commentaryProtos[i].amount += recordAmount;
        }
      }
    }
    if (record.debit_entity2) {
      for (let i = 0; i < filteringRules.length; i++) {
        if (filteringRules[i].rule(record.debit_entity2, record.debit_entity_sort_code2!)) {
          const sign = filteringRules[i].negate ? -1 : 1;
          const recordAmount = Number(record.debit_local_amount2) * sign * -1;
          updateRecords(
            commentaryProtos[i].amountsByEntity,
            recordAmount,
            record.debit_entity_sort_code2!,
            record.debit_entity2,
            record,
          );
          commentaryProtos[i].amount += recordAmount;
        }
      }
    }
  });

  return commentaryProtos.map(proto => ({
    amount: proto.amount,
    records: Array.from(proto.amountsByEntity.entries()).map(([sortCode, data]) => ({
      sortCode,
      amount: data.amount,
      records: Array.from(data.records.entries()).map(([financialEntityId, entityData]) => ({
        financialEntityId,
        amount: entityData.amount,
        ledgerRecords: Array.from(entityData.ledgerRecords.values()),
      })),
    })),
  })) as R;
}
