import { Injectable } from '@severo-tech/injection-decorator';
import cache from 'node-cache-decorator';

export interface ICombinationsValue {
  id: string;
  value: number | string;
}

@Injectable()
export class CombinationsUtil {
  @cache('CombinationsUtil.byValue', 60 * 60)
  public byValue<T extends ICombinationsValue>(list: T[], target: number, resolveIfDirectMatch = false): T[][] {
    const innerList = list
      .map((el: T): T & { value: number } => ({ ...el, value: +el.value }));

    if (resolveIfDirectMatch) {
      const directMatches = innerList.filter((el: T): boolean => el.value === target);

      if (directMatches.length) {
        return directMatches.map((el: T): T[] => [el]);
      }
    }

    const sortedList = innerList.sort((a: T, b: T): number => +b.value - +a.value);
    const combinations = this.findCombination(sortedList, target);

    return combinations.filter((el: T[]): boolean => el.length > 0);
  }

  private findCombination<T extends { value: number }>(list: T[], target: number): T[][] {
    let combinationsCount = 0;

    function innerFunction(current: T[], remaining: T[], remainingValue: number): T[][] {
      if (combinationsCount >= 100) {
        return [];
      }

      if (remainingValue === 0) {
        combinationsCount += 1;
        return [current];
      }

      if (remainingValue < 0) {
        return [];
      }

      const innerRemaining = remaining.filter((el: T): boolean => el.value <= remainingValue);
      if (innerRemaining.length === 0) {
        return [];
      }

      const sumOfRemaining = innerRemaining.reduce((total: number, el: T): number => +(total + el.value).toFixed(2), 0);
      if (sumOfRemaining < remainingValue) {
        return [];
      }

      const invoice = innerRemaining[0];
      const invoiceValue = invoice.value;

      const includeNote = innerFunction(
        [...current, invoice],
        innerRemaining.slice(1),
        +(remainingValue - invoiceValue).toFixed(2),
      );

      const excludeNote = innerFunction(
        current,
        innerRemaining.slice(1),
        remainingValue,
      );

      return [...includeNote, ...excludeNote];
    }

    return innerFunction([], list, target);
  }
}
