import { Inject, Service } from 'typedi';
import TenancyService from '@/services/Tenancy/TenancyService';
import { ServiceError } from '@/exceptions';
import { IAccountDTO, IAccount, IAccountCreateDTO } from '@/interfaces';
import AccountTypesUtils from '@/lib/AccountTypes';
import { ERRORS, MAX_ACCOUNTS_CHART_DEPTH } from './constants';

@Service()
export class CommandAccountValidators {
  @Inject()
  private tenancy: TenancyService;

  /**
   * Throws error if the account was prefined.
   * @param {IAccount} account
   */
  public throwErrorIfAccountPredefined(account: IAccount) {
    if (account.predefined) {
      throw new ServiceError(ERRORS.ACCOUNT_PREDEFINED);
    }
  }

  /**
   * Diff account type between new and old account, throw service error
   * if they have different account type.
   *
   * @param {IAccount|IAccountDTO} oldAccount
   * @param {IAccount|IAccountDTO} newAccount
   */
  public async isAccountTypeChangedOrThrowError(
    oldAccount: IAccount | IAccountDTO,
    newAccount: IAccount | IAccountDTO
  ) {
    if (oldAccount.accountType !== newAccount.accountType) {
      throw new ServiceError(ERRORS.ACCOUNT_TYPE_NOT_ALLOWED_TO_CHANGE);
    }
  }

  /**
   * Retrieve account type or throws service error.
   * @param {number} tenantId -
   * @param {number} accountTypeId -
   * @return {IAccountType}
   */
  public getAccountTypeOrThrowError(accountTypeKey: string) {
    const accountType = AccountTypesUtils.getType(accountTypeKey);

    if (!accountType) {
      throw new ServiceError(ERRORS.ACCOUNT_TYPE_NOT_FOUND);
    }
    return accountType;
  }

  /**
   * Retrieve parent account or throw service error.
   * @param {number} tenantId
   * @param {number} accountId
   * @param {number} notAccountId
   */
  public async getParentAccountOrThrowError(
    tenantId: number,
    accountId: number,
    notAccountId?: number
  ) {
    const { Account } = this.tenancy.models(tenantId);

    const parentAccount = await Account.query()
      .findById(accountId)
      .onBuild((query) => {
        if (notAccountId) {
          query.whereNot('id', notAccountId);
        }
      });
    if (!parentAccount) {
      throw new ServiceError(ERRORS.PARENT_ACCOUNT_NOT_FOUND);
    }
    return parentAccount;
  }

  /**
   * Throws error if the account type was not unique on the storage.
   * @param {number} tenantId
   * @param {string} accountCode
   * @param {number} notAccountId
   */
  public async isAccountCodeUniqueOrThrowError(
    tenantId: number,
    accountCode: string,
    notAccountId?: number
  ) {
    const { Account } = this.tenancy.models(tenantId);

    const account = await Account.query()
      .where('code', accountCode)
      .onBuild((query) => {
        if (notAccountId) {
          query.whereNot('id', notAccountId);
        }
      });
    if (account.length > 0) {
      throw new ServiceError(
        ERRORS.ACCOUNT_CODE_NOT_UNIQUE,
        'Account code is not unique.'
      );
    }
  }

  /**
   * Validates the account name uniquiness.
   * @param {number} tenantId
   * @param {string} accountName
   * @param {number} notAccountId - Ignore the account id.
   */
  public async validateAccountNameUniquiness(
    tenantId: number,
    accountName: string,
    notAccountId?: number
  ) {
    const { Account } = this.tenancy.models(tenantId);

    const foundAccount = await Account.query()
      .findOne('name', accountName)
      .onBuild((query) => {
        if (notAccountId) {
          query.whereNot('id', notAccountId);
        }
      });
    if (foundAccount) {
      throw new ServiceError(
        ERRORS.ACCOUNT_NAME_NOT_UNIQUE,
        'Account name is not unique.'
      );
    }
  }

  /**
   * Validates the given account type supports multi-currency.
   * @param {IAccountDTO} accountDTO -
   */
  public validateAccountTypeSupportCurrency = (
    accountDTO: IAccountCreateDTO,
    baseCurrency: string
  ) => {
    // Can't continue to validate the type has multi-currency feature
    // if the given currency equals the base currency or not assigned.
    if (accountDTO.currencyCode === baseCurrency || !accountDTO.currencyCode) {
      return;
    }
    const meta = AccountTypesUtils.getType(accountDTO.accountType);

    // Throw error if the account type does not support multi-currency.
    if (!meta?.multiCurrency) {
      throw new ServiceError(ERRORS.ACCOUNT_TYPE_NOT_SUPPORTS_MULTI_CURRENCY);
    }
  };

  /**
   * Validates the account DTO currency code whether equals the currency code of
   * parent account.
   * @param  {IAccountCreateDTO} accountDTO
   * @param  {IAccount} parentAccount
   * @param  {string} baseCurrency -
   * @throws {ServiceError(ERRORS.ACCOUNT_CURRENCY_NOT_SAME_PARENT_ACCOUNT)}
   */
  public validateCurrentSameParentAccount = (
    accountDTO: IAccountCreateDTO,
    parentAccount: IAccount,
    baseCurrency: string
  ) => {
    // If the account DTO currency not assigned and the parent account has no base currency.
    if (
      !accountDTO.currencyCode &&
      parentAccount.currencyCode !== baseCurrency
    ) {
      throw new ServiceError(ERRORS.ACCOUNT_CURRENCY_NOT_SAME_PARENT_ACCOUNT);
    }
    // If the account DTO is assigned and not equals the currency code of parent account.
    if (
      accountDTO.currencyCode &&
      parentAccount.currencyCode !== accountDTO.currencyCode
    ) {
      throw new ServiceError(ERRORS.ACCOUNT_CURRENCY_NOT_SAME_PARENT_ACCOUNT);
    }
  };

  /**
   * Throws service error if parent account has different type.
   * @param {IAccountDTO} accountDTO
   * @param {IAccount} parentAccount
   */
  public throwErrorIfParentHasDiffType(
    accountDTO: IAccountDTO,
    parentAccount: IAccount
  ) {
    if (accountDTO.accountType !== parentAccount.accountType) {
      throw new ServiceError(ERRORS.PARENT_ACCOUNT_HAS_DIFFERENT_TYPE);
    }
  }

  /**
   * Retrieve account of throw service error in case account not found.
   * @param {number} tenantId
   * @param {number} accountId
   * @return {IAccount}
   */
  public async getAccountOrThrowError(tenantId: number, accountId: number) {
    const { accountRepository } = this.tenancy.repositories(tenantId);

    const account = await accountRepository.findOneById(accountId);

    if (!account) {
      throw new ServiceError(ERRORS.ACCOUNT_NOT_FOUND);
    }
    return account;
  }

  /**
   * Validates the max depth level of accounts chart.
   * @param {numebr} tenantId - Tenant id.
   * @param {number} parentAccountId - Parent account id.
   */
  public async validateMaxParentAccountDepthLevels(
    tenantId: number,
    parentAccountId: number
  ) {
    const { accountRepository } = this.tenancy.repositories(tenantId);

    const accountsGraph = await accountRepository.getDependencyGraph();

    const parentDependantsIds = accountsGraph.dependantsOf(parentAccountId);

    if (parentDependantsIds.length >= MAX_ACCOUNTS_CHART_DEPTH) {
      throw new ServiceError(ERRORS.PARENT_ACCOUNT_EXCEEDED_THE_DEPTH_LEVEL);
    }
  }
}
