import * as R from 'ramda';
import { Knex } from 'knex';
import {
  ISaleInvoice,
  IItemEntry,
  ILedgerEntry,
  AccountNormal,
  ILedger,
} from '@/interfaces';
import { Service, Inject } from 'typedi';
import Ledger from '@/services/Accounting/Ledger';
import LedgerStorageService from '@/services/Accounting/LedgerStorageService';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import ItemsEntriesService from '@/services/Items/ItemsEntriesService';

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

  @Inject()
  private ledegrRepository: LedgerStorageService;

  @Inject()
  private itemsEntriesService: ItemsEntriesService;

  /**
   * Writes a sale invoice GL entries.
   * @param {number} tenantId - Tenant id.
   * @param {number} saleInvoiceId - Sale invoice id.
   * @param {Knex.Transaction} trx
   */
  public writeInvoiceGLEntries = async (
    tenantId: number,
    saleInvoiceId: number,
    trx?: Knex.Transaction
  ) => {
    const { SaleInvoice } = this.tenancy.models(tenantId);
    const { accountRepository } = this.tenancy.repositories(tenantId);

    const saleInvoice = await SaleInvoice.query(trx)
      .findById(saleInvoiceId)
      .withGraphFetched('entries.item');

    // Find or create the A/R account.
    const ARAccount = await accountRepository.findOrCreateAccountReceivable(
      saleInvoice.currencyCode, {}, trx
    );
    // Find or create tax payable account.
    const taxPayableAccount = await accountRepository.findOrCreateTaxPayable(
      {},
      trx
    );
    // Retrieves the ledger of the invoice.
    const ledger = this.getInvoiceGLedger(
      saleInvoice,
      ARAccount.id,
      taxPayableAccount.id
    );
    // Commits the ledger entries to the storage as UOW.
    await this.ledegrRepository.commit(tenantId, ledger, trx);
  };

  /**
   * Rewrites the given invoice GL entries.
   * @param {number} tenantId
   * @param {number} saleInvoiceId
   * @param {Knex.Transaction} trx
   */
  public rewritesInvoiceGLEntries = async (
    tenantId: number,
    saleInvoiceId: number,
    trx?: Knex.Transaction
  ) => {
    // Reverts the invoice GL entries.
    await this.revertInvoiceGLEntries(tenantId, saleInvoiceId, trx);

    // Writes the invoice GL entries.
    await this.writeInvoiceGLEntries(tenantId, saleInvoiceId, trx);
  };

  /**
   * Reverts the given invoice GL entries.
   * @param {number} tenantId
   * @param {number} saleInvoiceId
   * @param {Knex.Transaction} trx
   */
  public revertInvoiceGLEntries = async (
    tenantId: number,
    saleInvoiceId: number,
    trx?: Knex.Transaction
  ) => {
    await this.ledegrRepository.deleteByReference(
      tenantId,
      saleInvoiceId,
      'SaleInvoice',
      trx
    );
  };

  /**
   * Retrieves the given invoice ledger.
   * @param {ISaleInvoice} saleInvoice
   * @param {number} ARAccountId
   * @returns {ILedger}
   */
  public getInvoiceGLedger = (
    saleInvoice: ISaleInvoice,
    ARAccountId: number,
    taxPayableAccountId: number
  ): ILedger => {
    const entries = this.getInvoiceGLEntries(
      saleInvoice,
      ARAccountId,
      taxPayableAccountId
    );
    return new Ledger(entries);
  };

  /**
   * Retrieves the invoice GL common entry.
   * @param {ISaleInvoice} saleInvoice
   * @returns {Partial<ILedgerEntry>}
   */
  private getInvoiceGLCommonEntry = (
    saleInvoice: ISaleInvoice
  ): Partial<ILedgerEntry> => ({
    credit: 0,
    debit: 0,
    currencyCode: saleInvoice.currencyCode,
    exchangeRate: saleInvoice.exchangeRate,

    transactionType: 'SaleInvoice',
    transactionId: saleInvoice.id,

    date: saleInvoice.invoiceDate,
    userId: saleInvoice.userId,

    transactionNumber: saleInvoice.invoiceNo,
    referenceNumber: saleInvoice.referenceNo,

    createdAt: saleInvoice.createdAt,
    indexGroup: 10,

    branchId: saleInvoice.branchId,
  });

  /**
   * Retrieve receivable entry of the given invoice.
   * @param {ISaleInvoice} saleInvoice
   * @param {number} ARAccountId
   * @returns {ILedgerEntry}
   */
  private getInvoiceReceivableEntry = (
    saleInvoice: ISaleInvoice,
    ARAccountId: number
  ): ILedgerEntry => {
    const commonEntry = this.getInvoiceGLCommonEntry(saleInvoice);

    return {
      ...commonEntry,
      debit: saleInvoice.totalLocal,
      accountId: ARAccountId,
      contactId: saleInvoice.customerId,
      accountNormal: AccountNormal.DEBIT,
      index: 1,
    } as ILedgerEntry;
  };

  /**
   * Retrieve item income entry of the given invoice.
   * @param {ISaleInvoice} saleInvoice -
   * @param {IItemEntry} entry -
   * @param {number} index -
   * @returns {ILedgerEntry}
   */
  private getInvoiceItemEntry = R.curry(
    (
      saleInvoice: ISaleInvoice,
      entry: IItemEntry,
      index: number
    ): ILedgerEntry => {
      const commonEntry = this.getInvoiceGLCommonEntry(saleInvoice);
      const localAmount = entry.amountExludingTax * saleInvoice.exchangeRate;

      return {
        ...commonEntry,
        credit: localAmount,
        accountId: entry.sellAccountId,
        note: entry.description,
        index: index + 2,
        itemId: entry.itemId,
        itemQuantity: entry.quantity,
        accountNormal: AccountNormal.CREDIT,
        projectId: entry.projectId || saleInvoice.projectId,
        taxRateId: entry.taxRateId,
        taxRate: entry.taxRate,
      };
    }
  );

  /**
   * Retreives the GL entry of tax payable.
   * @param {ISaleInvoice} saleInvoice -
   * @param {number} taxPayableAccountId -
   * @returns {ILedgerEntry}
   */
  private getInvoiceTaxEntry = R.curry(
    (
      saleInvoice: ISaleInvoice,
      taxPayableAccountId: number,
      entry: IItemEntry,
      index: number
    ): ILedgerEntry => {
      const commonEntry = this.getInvoiceGLCommonEntry(saleInvoice);

      return {
        ...commonEntry,
        credit: entry.taxAmount,
        accountId: taxPayableAccountId,
        index: index + 1,
        indexGroup: 30,
        accountNormal: AccountNormal.CREDIT,
        taxRateId: entry.taxRateId,
        taxRate: entry.taxRate,
      };
    }
  );

  /**
   * Retrieves the invoice tax GL entries.
   * @param {ISaleInvoice} saleInvoice
   * @param {number} taxPayableAccountId
   * @returns {ILedgerEntry[]}
   */
  private getInvoiceTaxEntries = (
    saleInvoice: ISaleInvoice,
    taxPayableAccountId: number
  ): ILedgerEntry[] => {
    // Retrieves the non-zero tax entries.
    const nonZeroTaxEntries = this.itemsEntriesService.getNonZeroEntries(
      saleInvoice.entries
    );
    const transformTaxEntry = this.getInvoiceTaxEntry(
      saleInvoice,
      taxPayableAccountId
    );
    // Transforms the non-zero tax entries to GL entries.
    return nonZeroTaxEntries.map(transformTaxEntry);
  };

  /**
   * Retrieves the invoice GL entries.
   * @param {ISaleInvoice} saleInvoice
   * @param {number} ARAccountId
   * @returns {ILedgerEntry[]}
   */
  public getInvoiceGLEntries = (
    saleInvoice: ISaleInvoice,
    ARAccountId: number,
    taxPayableAccountId: number
  ): ILedgerEntry[] => {
    const receivableEntry = this.getInvoiceReceivableEntry(
      saleInvoice,
      ARAccountId
    );
    const transformItemEntry = this.getInvoiceItemEntry(saleInvoice);
    const creditEntries = saleInvoice.entries.map(transformItemEntry);

    const taxEntries = this.getInvoiceTaxEntries(
      saleInvoice,
      taxPayableAccountId
    );
    return [receivableEntry, ...creditEntries, ...taxEntries];
  };
}
