import { Service, Inject } from 'typedi';
import { pick } from 'lodash';
import { Knex } from 'knex';
import * as R from 'ramda';
import {
  ICashflowNewCommandDTO,
  ICashflowTransaction,
  ICommandCashflowCreatedPayload,
  ICommandCashflowCreatingPayload,
  ICashflowTransactionInput,
  IAccount,
} from '@/interfaces';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { CASHFLOW_TRANSACTION_TYPE } from './constants';
import { transformCashflowTransactionType } from './utils';
import events from '@/subscribers/events';
import { CommandCashflowValidator } from './CommandCasflowValidator';
import UnitOfWork from '@/services/UnitOfWork';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import { CashflowTransactionAutoIncrement } from './CashflowTransactionAutoIncrement';
import { BranchTransactionDTOTransform } from '@/services/Branches/Integrations/BranchTransactionDTOTransform';

@Service()
export default class NewCashflowTransactionService {
  @Inject()
  private tenancy: HasTenancyService;

  @Inject()
  private validator: CommandCashflowValidator;

  @Inject()
  private uow: UnitOfWork;

  @Inject()
  private eventPublisher: EventPublisher;

  @Inject()
  private autoIncrement: CashflowTransactionAutoIncrement;

  @Inject()
  private branchDTOTransform: BranchTransactionDTOTransform;

  /**
   * Authorize the cashflow creating transaction.
   * @param {number} tenantId
   * @param {ICashflowNewCommandDTO} newCashflowTransactionDTO
   */
  public authorize = async (
    tenantId: number,
    newCashflowTransactionDTO: ICashflowNewCommandDTO,
    creditAccount: IAccount
  ) => {
    const transactionType = transformCashflowTransactionType(
      newCashflowTransactionDTO.transactionType
    );
    // Validates the cashflow transaction type.
    this.validator.validateCashflowTransactionType(transactionType);

    // Retrieve accounts of the cashflow lines object.
    this.validator.validateCreditAccountWithCashflowType(
      creditAccount,
      transactionType as CASHFLOW_TRANSACTION_TYPE
    );
  };

  /**
   * Transformes owner contribution DTO to cashflow transaction.
   * @param {ICashflowNewCommandDTO} newCashflowTransactionDTO - New transaction DTO.
   * @returns {ICashflowTransaction} - Cashflow transaction object.
   */
  private transformCashflowTransactionDTO = (
    tenantId: number,
    newCashflowTransactionDTO: ICashflowNewCommandDTO,
    cashflowAccount: IAccount,
    userId: number
  ): ICashflowTransactionInput => {
    const amount = newCashflowTransactionDTO.amount;

    const fromDTO = pick(newCashflowTransactionDTO, [
      'date',
      'referenceNo',
      'description',
      'transactionType',
      'exchangeRate',
      'cashflowAccountId',
      'creditAccountId',
      'branchId',
      'plaidTransactionId',
      'uncategorizedTransactionId',
    ]);
    // Retreive the next invoice number.
    const autoNextNumber =
      this.autoIncrement.getNextTransactionNumber(tenantId);

    // Retrieve the transaction number.
    const transactionNumber =
      newCashflowTransactionDTO.transactionNumber || autoNextNumber;

    const initialDTO = {
      amount,
      ...fromDTO,
      transactionNumber,
      currencyCode: cashflowAccount.currencyCode,
      exchangeRate: fromDTO?.exchangeRate || 1,
      transactionType: transformCashflowTransactionType(
        fromDTO.transactionType
      ),
      userId,
      ...(newCashflowTransactionDTO.publish
        ? {
            publishedAt: new Date(),
          }
        : {}),
    };
    return R.compose(
      this.branchDTOTransform.transformDTO<ICashflowTransactionInput>(tenantId)
    )(initialDTO);
  };

  /**
   * Owner contribution money in.
   * @param {number} tenantId -
   * @param {ICashflowOwnerContributionDTO} ownerContributionDTO
   * @param {number} userId - User id.
   */
  public newCashflowTransaction = async (
    tenantId: number,
    newTransactionDTO: ICashflowNewCommandDTO,
    userId?: number
  ): Promise<ICashflowTransaction> => {
    const { CashflowTransaction, Account } = this.tenancy.models(tenantId);

    // Retrieves the cashflow account or throw not found error.
    const cashflowAccount = await Account.query()
      .findById(newTransactionDTO.cashflowAccountId)
      .throwIfNotFound();

    // Retrieves the credit account or throw not found error.
    const creditAccount = await Account.query()
      .findById(newTransactionDTO.creditAccountId)
      .throwIfNotFound();

    // Authorize before creating cashflow transaction.
    await this.authorize(tenantId, newTransactionDTO, creditAccount);

    // Transformes owner contribution DTO to cashflow transaction.
    const cashflowTransactionObj = this.transformCashflowTransactionDTO(
      tenantId,
      newTransactionDTO,
      cashflowAccount,
      userId
    );
    // Creates a new cashflow transaction under UOW envirement.
    return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
      // Triggers `onCashflowTransactionCreate` event.
      await this.eventPublisher.emitAsync(
        events.cashflow.onTransactionCreating,
        {
          trx,
          tenantId,
          newTransactionDTO,
        } as ICommandCashflowCreatingPayload
      );
      // Inserts cashflow owner contribution transaction.
      const cashflowTransaction = await CashflowTransaction.query(
        trx
      ).upsertGraph(cashflowTransactionObj);

      // Triggers `onCashflowTransactionCreated` event.
      await this.eventPublisher.emitAsync(
        events.cashflow.onTransactionCreated,
        {
          tenantId,
          newTransactionDTO,
          cashflowTransaction,
          trx,
        } as ICommandCashflowCreatedPayload
      );
      return cashflowTransaction;
    });
  };
}
