import { flatten, pick, sumBy } from 'lodash';
import { IProfitLossSheetQuery } from 'interfaces/ProfitLossSheet';
import FinancialSheet from '../FinancialSheet';
import {
  IAccount,
  IAccountType,
  IJournalPoster,
  IProfitLossSheetAccount,
  IProfitLossSheetTotal,
  IProfitLossSheetStatement,
  IProfitLossSheetAccountsSection,
  IProfitLossSheetTotalSection,
} from 'interfaces';
import { flatToNestedArray, dateRangeCollection } from 'utils';
import { ACCOUNT_TYPE } from 'data/AccountTypes';

export default class ProfitLossSheet extends FinancialSheet {
  tenantId: number;
  query: IProfitLossSheetQuery;
  accounts: IAccount & { type: IAccountType }[];
  journal: IJournalPoster;
  dateRangeSet: string[];
  comparatorDateType: string;
  baseCurrency: string;

  /**
   * Constructor method.
   * @param {number} tenantId -
   * @param {IProfitLossSheetQuery} query -
   * @param {IAccount[]} accounts -
   * @param {IJournalPoster} transactionsJournal -
   */
  constructor(
    tenantId: number,
    query: IProfitLossSheetQuery,
    accounts: IAccount & { type: IAccountType }[],
    journal: IJournalPoster,
    baseCurrency: string
  ) {
    super();

    this.tenantId = tenantId;
    this.query = query;
    this.numberFormat = this.query.numberFormat;
    this.accounts = accounts;
    this.journal = journal;
    this.baseCurrency = baseCurrency;
    this.comparatorDateType =
      query.displayColumnsType === 'total' ? 'day' : query.displayColumnsBy;

    this.initDateRangeCollection();
  }

  get otherIncomeAccounts() {
    return this.accounts.filter(
      (a) => a.accountType === ACCOUNT_TYPE.OTHER_INCOME
    );
  }

  /**
   * Filtering income accounts.
   * @return {IAccount & { type: IAccountType }[]}
   */
  get incomeAccounts() {
    return this.accounts.filter((a) => a.accountType === ACCOUNT_TYPE.INCOME);
  }

  /**
   * Filtering expenses accounts.
   * @return {IAccount & { type: IAccountType }[]}
   */
  get expensesAccounts() {
    return this.accounts.filter((a) => a.accountType === ACCOUNT_TYPE.EXPENSE);
  }

  /**
   * Filter other expenses accounts.
   * @return {IAccount & { type: IAccountType }[]}}
   */
  get otherExpensesAccounts() {
    return this.accounts.filter(
      (a) => a.accountType === ACCOUNT_TYPE.OTHER_EXPENSE
    );
  }

  /**
   * Filtering cost of sales accounts.
   * @return {IAccount & { type: IAccountType }[]}
   */
  get costOfSalesAccounts() {
    return this.accounts.filter(
      (a) => a.accountType === ACCOUNT_TYPE.COST_OF_GOODS_SOLD
    );
  }

  /**
   * Initialize date range set.
   */
  initDateRangeCollection() {
    if (this.query.displayColumnsType === 'date_periods') {
      this.dateRangeSet = dateRangeCollection(
        this.query.fromDate,
        this.query.toDate,
        this.comparatorDateType
      );
    }
  }

  /**
   * Retrieve account total in the query date.
   * @param  {IAccount} account -
   * @return {IProfitLossSheetTotal}
   */
  private getAccountTotal(account: IAccount): IProfitLossSheetTotal {
    const amount = this.journal.getAccountBalance(
      account.id,
      this.query.toDate,
      this.comparatorDateType
    );
    const formattedAmount = this.formatNumber(amount);
    const currencyCode = this.baseCurrency;

    return { amount, formattedAmount, currencyCode };
  }

  /**
   * Retrieve account total periods.
   * @param {IAccount} account -
   * @return {IProfitLossSheetTotal[]}
   */
  private getAccountTotalPeriods(account: IAccount): IProfitLossSheetTotal[] {
    return this.dateRangeSet.map((date) => {
      const amount = this.journal.getAccountBalance(
        account.id,
        date,
        this.comparatorDateType
      );
      const formattedAmount = this.formatNumber(amount);
      const currencyCode = this.baseCurrency;

      return { date, amount, formattedAmount, currencyCode };
    });
  }

  /**
   * Mapping the given account to total result with account metadata.
   * @param  {IAccount} account -
   * @return {IProfitLossSheetAccount}
   */
  private accountMapper(account: IAccount): IProfitLossSheetAccount {
    const entries = this.journal.getAccountEntries(account.id);

    return {
      ...pick(account, ['id', 'index', 'name', 'code', 'parentAccountId']),
      hasTransactions: entries.length > 0,
      total: this.getAccountTotal(account),

      // Date periods when display columns type `periods`.
      ...(this.query.displayColumnsType === 'date_periods' && {
        totalPeriods: this.getAccountTotalPeriods(account),
      }),
    };
  }

  /**
   *
   * @param {IAccount[]} accounts -
   * @return {IProfitLossSheetAccount[]}
   */
  private accountsWalker(
    accounts: IAccount & { type: IAccountType }[]
  ): IProfitLossSheetAccount[] {
    const flattenAccounts = accounts
      .map(this.accountMapper.bind(this))
      // Filter accounts that have no transaction when `noneTransactions` is on.
      .filter(
        (account: IProfitLossSheetAccount) =>
          !(!account.hasTransactions && this.query.noneTransactions)
      )
      // Filter accounts that have zero total amount when `noneZero` is on.
      .filter(
        (account: IProfitLossSheetAccount) =>
          !(account.total.amount === 0 && this.query.noneZero)
      );

    return flatToNestedArray(flattenAccounts, {
      id: 'id',
      parentId: 'parentAccountId',
    });
  }

  /**
   * Retreive the report total section.
   * @param {IAccount[]} accounts -
   * @return {IProfitLossSheetTotal}
   */
  private gatTotalSection(
    accounts: IProfitLossSheetAccount[]
  ): IProfitLossSheetTotal {
    const amount = sumBy(accounts, 'total.amount');
    const formattedAmount = this.formatTotalNumber(amount);
    const currencyCode = this.baseCurrency;

    return { amount, formattedAmount, currencyCode };
  }

  /**
   * Retrieve the report total section in periods display type.
   * @param {IAccount} accounts -
   * @return {IProfitLossSheetTotal[]}
   */
  private getTotalPeriodsSection(
    accounts: IProfitLossSheetAccount[]
  ): IProfitLossSheetTotal[] {
    return this.dateRangeSet.map((date, index) => {
      const amount = sumBy(accounts, `totalPeriods[${index}].amount`);
      const formattedAmount = this.formatTotalNumber(amount);
      const currencyCode = this.baseCurrency;

      return { amount, formattedAmount, currencyCode };
    });
  }

  sectionMapper(sectionAccounts) {
    const accounts = this.accountsWalker(sectionAccounts);
    const total = this.gatTotalSection(accounts);

    return {
      accounts,
      total,
      ...(this.query.displayColumnsType === 'date_periods' && {
        totalPeriods: this.getTotalPeriodsSection(accounts),
      }),
    };
  }

  /**
   * Retrieve income section.
   * @return {IProfitLossSheetIncomeSection}
   */
  private get incomeSection(): IProfitLossSheetAccountsSection {
    return {
      name: 'Income accounts',
      entryNormal: 'credit',
      ...this.sectionMapper(this.incomeAccounts),
    };
  }

  private get otherIncomeSection(): any {
    return {
      name: 'Other Income',
      entryNormal: 'credit',
      ...this.sectionMapper(this.otherIncomeAccounts),
    };
  }

  /**
   * Retreive expenses section.
   * @return {IProfitLossSheetLossSection}
   */
  private get expensesSection(): IProfitLossSheetAccountsSection {
    return {
      name: 'Expense accounts',
      entryNormal: 'debit',
      ...this.sectionMapper(this.expensesAccounts),
    };
  }

  /**
   * Retrieve other expenses section.
   * @return {IProfitLossSheetAccountsSection}
   */
  private get otherExpensesSection(): IProfitLossSheetAccountsSection {
    return {
      name: 'Other expenses accounts',
      entryNormal: 'debit',
      ...this.sectionMapper(this.otherExpensesAccounts),
    };
  }

  /**
   * Cost of sales section.
   * @return {IProfitLossSheetAccountsSection}
   */
  private get costOfSalesSection(): IProfitLossSheetAccountsSection {
    return {
      name: 'Cost of sales',
      entryNormal: 'debit',
      ...this.sectionMapper(this.costOfSalesAccounts),
    };
  }

  private getSummarySectionDatePeriods(
    positiveSections: IProfitLossSheetTotalSection[],
    minesSections: IProfitLossSheetTotalSection[]
  ) {
    return this.dateRangeSet.map((date, index: number) => {
      const totalPositive = sumBy(
        positiveSections,
        `totalPeriods[${index}].amount`
      );
      const totalMines = sumBy(minesSections, `totalPeriods[${index}].amount`);

      const amount = totalPositive - totalMines;
      const formattedAmount = this.formatTotalNumber(amount);
      const currencyCode = this.baseCurrency;

      return { date, amount, formattedAmount, currencyCode };
    });
  }

  private getSummarySectionTotal(
    positiveSections: IProfitLossSheetTotalSection[],
    minesSections: IProfitLossSheetTotalSection[]
  ) {
    const totalPositiveSections = sumBy(positiveSections, 'total.amount');
    const totalMinesSections = sumBy(minesSections, 'total.amount');

    const amount = totalPositiveSections - totalMinesSections;
    const formattedAmount = this.formatTotalNumber(amount);
    const currencyCode = this.baseCurrency;

    return { amount, formattedAmount, currencyCode };
  }

  /**
   * Retrieve the summary section
   * @param
   */
  private getSummarySection(
    sections: IProfitLossSheetTotalSection | IProfitLossSheetTotalSection[],
    subtractSections:
      | IProfitLossSheetTotalSection
      | IProfitLossSheetTotalSection[]
  ): IProfitLossSheetTotalSection {
    const positiveSections = Array.isArray(sections) ? sections : [sections];
    const minesSections = Array.isArray(subtractSections)
      ? subtractSections
      : [subtractSections];

    return {
      total: this.getSummarySectionTotal(positiveSections, minesSections),
      ...(this.query.displayColumnsType === 'date_periods' && {
        totalPeriods: [
          ...this.getSummarySectionDatePeriods(positiveSections, minesSections),
        ],
      }),
    };
  }

  /**
   * Retrieve date range columns of the given query.
   * @param {IBalanceSheetQuery} query
   * @return {string[]}
   */
  private dateRangeColumns(): string[] {
    return this.dateRangeSet;
  }

  /**
   * Retrieve profit/loss report data.
   * @return {IProfitLossSheetStatement}
   */
  public reportData(): IProfitLossSheetStatement {
    if (this.journal.isEmpty()) {
      return null;
    }
    const income = this.incomeSection;
    const costOfSales = this.costOfSalesSection;
    const expenses = this.expensesSection;
    const otherExpenses = this.otherExpensesSection;
    const otherIncome = this.otherIncomeSection;

    // - Gross profit = Total income - COGS.
    const grossProfit = this.getSummarySection(income, costOfSales);

    // - Operating profit = Gross profit - Expenses.
    const operatingProfit = this.getSummarySection(grossProfit, expenses);

    // Net other income = Other income - other expenses.
    const netOtherIncome = this.getSummarySection(otherIncome, otherExpenses);

    // - Net income = (Operating profit + other income) - Other expenses.
    const netIncome = this.getSummarySection(
      [operatingProfit, netOtherIncome],
      [],
    );

    return {
      income,
      costOfSales,
      grossProfit,
      expenses,
      otherIncome,
      otherExpenses,
      operatingProfit,
      netOtherIncome,
      netIncome,
    };
  }

  /**
   * Retrieve profit/loss report columns.
   */
  public reportColumns() {
    // Date range collection.
    return this.query.displayColumnsType === 'date_periods'
      ? this.dateRangeColumns()
      : ['total'];
  }
}
