import { sumBy } from 'lodash';
import * as R from 'ramda';
import {
  ITrialBalanceSheetQuery,
  ITrialBalanceAccount,
  ITrialBalanceTotal,
  ITrialBalanceSheetData,
} from './TrialBalanceSheet.types';
import { TrialBalanceSheetRepository } from './TrialBalanceSheetRepository';
import { FinancialSheet } from '../../common/FinancialSheet';
import { Account } from '@/modules/Accounts/models/Account.model';
import { allPassedConditionsPass } from '@/utils/all-conditions-passed';
import { ModelObject } from 'objection';
import { flatToNestedArray } from '@/utils/flat-to-nested-array';
import { IFinancialReportMeta, DEFAULT_REPORT_META } from '../../types/Report.types';

export class TrialBalanceSheet extends FinancialSheet {
  /**
   * Trial balance sheet query.
   * @param {ITrialBalanceSheetQuery} query
   */
  public query: ITrialBalanceSheetQuery;

  /**
   * Trial balance sheet repository.
   * @param {TrialBalanceSheetRepository}
   */
  public repository: TrialBalanceSheetRepository;

  /**
   * Organization base currency.
   * @param {string}
   */
  public baseCurrency: string;

  /**
   * Constructor method.
   * @param {number} tenantId
   * @param {ITrialBalanceSheetQuery} query
   * @param {IAccount[]} accounts
   * @param journalFinancial
   */
  constructor(
    query: ITrialBalanceSheetQuery,
    repository: TrialBalanceSheetRepository,
    meta: IFinancialReportMeta,
  ) {
    super();

    this.query = query;
    this.repository = repository;
    this.numberFormat = this.query.numberFormat;
    this.baseCurrency = meta.baseCurrency;
    this.dateFormat = meta.dateFormat || DEFAULT_REPORT_META.dateFormat;
  }

  /**
   * Retrieves the closing credit of the given account.
   * @param {number} accountId
   * @returns {number}
   */
  public getClosingAccountCredit(accountId: number) {
    const depsAccountsIds =
      this.repository.accountsDepGraph.dependenciesOf(accountId);

    return this.repository.totalAccountsLedger
      .whereAccountsIds([accountId, ...depsAccountsIds])
      .getClosingCredit();
  }

  /**
   * Retrieves the closing debit of the given account.
   * @param {number} accountId
   * @returns {number}
   */
  public getClosingAccountDebit(accountId: number) {
    const depsAccountsIds =
      this.repository.accountsDepGraph.dependenciesOf(accountId);

    return this.repository.totalAccountsLedger
      .whereAccountsIds([accountId, ...depsAccountsIds])
      .getClosingDebit();
  }

  /**
   * Retrieves the closing total of the given account.
   * @param {number} accountId
   * @returns {number}
   */
  public getClosingAccountTotal(accountId: number) {
    const credit = this.getClosingAccountCredit(accountId);
    const debit = this.getClosingAccountDebit(accountId);

    return debit - credit;
  }

  /**
   * Account mapper.
   * @param {IAccount} account
   * @return {ITrialBalanceAccount}
   */
  private accountTransformer = (
    account: Account
  ): ITrialBalanceAccount => {
    const debit = this.getClosingAccountDebit(account.id);
    const credit = this.getClosingAccountCredit(account.id);
    const balance = this.getClosingAccountTotal(account.id);

    return {
      id: account.id,
      parentAccountId: account.parentAccountId,
      name: account.name,
      formattedName: account.code
        ? `${account.name} - ${account.code}`
        : `${account.name}`,
      code: account.code,
      accountNormal: account.accountNormal,

      credit,
      debit,
      balance,
      currencyCode: this.baseCurrency,

      formattedCredit: this.formatNumber(credit),
      formattedDebit: this.formatNumber(debit),
      formattedBalance: this.formatNumber(balance),
    };
  };

  /**
   * Filters trial balance sheet accounts nodes based on the given report query.
   * @param {ITrialBalanceAccount} accountNode
   * @returns {boolean}
   */
  private accountFilter = (accountNode: ITrialBalanceAccount): boolean => {
    const { noneTransactions, noneZero, onlyActive } = this.query;

    // Conditions pair filter detarminer.
    const condsPairFilters = [
      [noneTransactions, this.filterNoneTransactions],
      [noneZero, this.filterNoneZero],
      [onlyActive, this.filterActiveOnly],
    ];
    return allPassedConditionsPass(condsPairFilters)(accountNode);
  };

  /**
   * Fitlers the accounts nodes.
   * @param {ITrialBalanceAccount[]} accountsNodes
   * @returns {ITrialBalanceAccount[]}
   */
  private accountsFilter = (
    accountsNodes: ITrialBalanceAccount[]
  ): ITrialBalanceAccount[] => {
    return accountsNodes.filter(this.accountFilter);
  };

  /**
   * Mappes the given account object to trial balance account node.
   * @param {IAccount[]} accountsNodes
   * @returns {ITrialBalanceAccount[]}
   */
  private accountsMapper = (
    accountsNodes: ModelObject<Account>[]
  ): ITrialBalanceAccount[] => {
    return accountsNodes.map(this.accountTransformer);
  };

  /**
   * Detarmines whether the given account node is not none transactions.
   * @param {ITrialBalanceAccount} accountNode
   * @returns {boolean}
   */
  private filterNoneTransactions = (
    accountNode: ITrialBalanceAccount
  ): boolean => {
    const depsAccountsIds =
      this.repository.accountsDepGraph.dependenciesOf(accountNode.id);

    const accountLedger = this.repository.totalAccountsLedger.whereAccountsIds(
      [accountNode.id, ...depsAccountsIds]
    );
    return !accountLedger.isEmpty();
  };

  /**
   * Detarmines whether the given account none zero.
   * @param {ITrialBalanceAccount} accountNode
   * @returns {boolean}
   */
  private filterNoneZero = (accountNode: ITrialBalanceAccount): boolean => {
    return accountNode.balance !== 0;
  };

  /**
   * Detarmines whether the given account is active.
   * @param {ITrialBalanceAccount} accountNode
   * @returns {boolean}
   */
  private filterActiveOnly = (accountNode: ITrialBalanceAccount): boolean => {
    return accountNode.credit !== 0 || accountNode.debit !== 0;
  };

  /**
   * Transformes the flatten nodes to nested nodes.
   * @param {ITrialBalanceAccount[]} flattenAccounts
   * @returns {ITrialBalanceAccount[]}
   */
  private nestedAccountsNode = (
    flattenAccounts: ITrialBalanceAccount[]
  ): ITrialBalanceAccount[] => {
    return flatToNestedArray(flattenAccounts, {
      id: 'id',
      parentId: 'parentAccountId',
    });
  };

  /**
   * Retrieve trial balance total section.
   * @param {ITrialBalanceAccount[]} accountsBalances
   * @return {ITrialBalanceTotal}
   */
  private tatalSection(
    accountsBalances: ITrialBalanceAccount[]
  ): ITrialBalanceTotal {
    const credit = sumBy(accountsBalances, 'credit');
    const debit = sumBy(accountsBalances, 'debit');
    const balance = sumBy(accountsBalances, 'balance');
    const currencyCode = this.baseCurrency;

    return {
      credit,
      debit,
      balance,
      currencyCode,
      formattedCredit: this.formatTotalNumber(credit),
      formattedDebit: this.formatTotalNumber(debit),
      formattedBalance: this.formatTotalNumber(balance),
    };
  }

  /**
   * Retrieve accounts section of trial balance report.
   * @param {IAccount[]} accounts
   * @returns {ITrialBalanceAccount[]}
   */
  private accountsSection(accounts: ModelObject<Account>[]) {
    return R.compose(
      this.accountsFilter,
      this.nestedAccountsNode,
      this.accountsMapper
    )(accounts);
  }

  /**
   * Retrieve trial balance sheet statement data.
   * Note: Retruns null in case there is no transactions between the given date periods.
   * @return {ITrialBalanceSheetData}
   */
  public reportData(): ITrialBalanceSheetData {
    // Retrieve accounts nodes.
    const accounts = this.accountsSection(this.repository.accounts);

    // Retrieve account node.
    const total = this.tatalSection(accounts);

    return { accounts, total };
  }
}
