import { Fyo, t } from 'fyo';
import { NotFoundError, ValidationError } from 'fyo/utils/errors';
import { AccountingLedgerEntry } from 'models/baseModels/AccountingLedgerEntry/AccountingLedgerEntry';
import { ModelNameEnum } from 'models/types';
import { Money } from 'pesa';
import { Transactional } from './Transactional';
import { TransactionType } from './types';

/**
 * # LedgerPosting
 *
 * Class that maintains a set of AccountingLedgerEntries pertaining to
 * a single Transactional doc, example a Payment entry.
 *
 * For each account touched in a transactional doc a separate ledger entry is
 * created.
 *
 * This is done using the `debit(...)` and `credit(...)` methods.
 *
 * Unless `post()` or `postReverese()` is called the entries aren't made.
 */

export class LedgerPosting {
  fyo: Fyo;
  refDoc: Transactional;
  entries: AccountingLedgerEntry[];
  creditMap: Record<string, AccountingLedgerEntry>;
  debitMap: Record<string, AccountingLedgerEntry>;
  reverted: boolean;

  constructor(refDoc: Transactional, fyo: Fyo) {
    this.fyo = fyo;
    this.refDoc = refDoc;
    this.entries = [];
    this.creditMap = {};
    this.debitMap = {};
    this.reverted = false;
  }

  async debit(account: string, amount: Money) {
    const ledgerEntry = this._getLedgerEntry(account, 'debit');
    await ledgerEntry.set('debit', ledgerEntry.debit!.add(amount));
  }

  async credit(account: string, amount: Money) {
    const ledgerEntry = this._getLedgerEntry(account, 'credit');
    await ledgerEntry.set('credit', ledgerEntry.credit!.add(amount));
  }

  async post() {
    this._validateIsEqual();
    await this._sync();
  }

  async postReverse() {
    this._validateIsEqual();
    await this._syncReverse();
  }

  validate() {
    this._validateIsEqual();
  }

  async makeRoundOffEntry() {
    const { debit, credit } = this._getTotalDebitAndCredit();
    const difference = debit.sub(credit);
    const absoluteValue = difference.abs();
    if (absoluteValue.eq(0)) {
      return;
    }

    const roundOffAccount = await this._getRoundOffAccount();
    if (difference.gt(0)) {
      this.credit(roundOffAccount, absoluteValue);
    } else {
      this.debit(roundOffAccount, absoluteValue);
    }
  }

  _getLedgerEntry(
    account: string,
    type: TransactionType
  ): AccountingLedgerEntry {
    let map = this.creditMap;
    if (type === 'debit') {
      map = this.debitMap;
    }

    if (map[account]) {
      return map[account];
    }

    const ledgerEntry = this.fyo.doc.getNewDoc(
      ModelNameEnum.AccountingLedgerEntry,
      {
        account: account,
        party: (this.refDoc.party as string) ?? '',
        date: this.refDoc.date as string | Date,
        referenceType: this.refDoc.schemaName,
        referenceName: this.refDoc.name!,
        reverted: this.reverted,
        debit: this.fyo.pesa(0),
        credit: this.fyo.pesa(0),
      },
      false
    ) as AccountingLedgerEntry;

    this.entries.push(ledgerEntry);
    map[account] = ledgerEntry;

    return map[account];
  }

  _validateIsEqual() {
    const { debit, credit } = this._getTotalDebitAndCredit();
    if (debit.eq(credit)) {
      return;
    }

    throw new ValidationError(
      t`Total Debit: ${this.fyo.format(
        debit,
        'Currency'
      )} must be equal to Total Credit: ${this.fyo.format(credit, 'Currency')}`
    );
  }

  _getTotalDebitAndCredit() {
    let debit = this.fyo.pesa(0);
    let credit = this.fyo.pesa(0);

    for (const entry of this.entries) {
      debit = debit.add(entry.debit!);
      credit = credit.add(entry.credit!);
    }

    return { debit, credit };
  }

  async _sync() {
    await this._syncLedgerEntries();
  }

  async _syncLedgerEntries() {
    for (const entry of this.entries) {
      await entry.sync();
    }
  }

  async _syncReverse() {
    await this._syncReverseLedgerEntries();
  }

  async _syncReverseLedgerEntries() {
    const data = (await this.fyo.db.getAll('AccountingLedgerEntry', {
      fields: ['name'],
      filters: {
        referenceType: this.refDoc.schemaName,
        referenceName: this.refDoc.name!,
        reverted: false,
      },
    })) as { name: string }[];

    for (const { name } of data) {
      const doc = (await this.fyo.doc.getDoc(
        'AccountingLedgerEntry',
        name
      )) as AccountingLedgerEntry;
      await doc.revert();
    }
  }

  async _getRoundOffAccount() {
    const roundOffAccount = (await this.fyo.getValue(
      ModelNameEnum.AccountingSettings,
      'roundOffAccount'
    )) as string;

    if (!roundOffAccount) {
      const notFoundError = new NotFoundError(
        t`Please set Round Off Account in the Settings.`,
        false
      );
      notFoundError.name = t`Round Off Account Not Found`;
      throw notFoundError;
    }

    return roundOffAccount;
  }
}
