/* eslint-disable no-await-in-loop */
/* eslint-disable no-restricted-syntax */
import { Inject } from '@severo-tech/injection-decorator';
import { IConciliationClient, IInvoiceClient, ILogger } from '../../../contracts';
import { Invoice } from '../../../../domain/invoice';
import { CreateEustIssueUseCase } from '../..';
import { Debit } from '../../../../domain/debit';
import { InvoiceInCancellationPeriod, MultipleInvoices } from '../../../../domain/errors';
import { EustIssueType, EustIssueUnit } from '../../../../domain/eust-issue';
import { CombinationsUtil } from '../../../utils';

export abstract class ConciliationBase {
  @Inject('ZenviaLogger')
  protected readonly logger!: ILogger;

  @Inject('Invoice01EUSTClient')
  private readonly innerInvoiceClient!: IInvoiceClient;

  @Inject('Conciliation01EUSTClient')
  private readonly innerConciliationClient!: IConciliationClient;

  @Inject('CreateEustIssueUseCase')
  private readonly innerCreateEustIssueUseCase!: CreateEustIssueUseCase;

  @Inject('CombinationsUtil')
  private readonly innerCombinationsUtil!: CombinationsUtil;

  protected async tryToConciliateDebits(startDate: Date, endDate: Date, debits: Debit[], emitterDocument: string, recipientDocument: string, value: number): Promise<void> {
    try {
      const invoices = await this.getInvoicesToConciliate(startDate, endDate, emitterDocument, recipientDocument, value);

      if (invoices?.length) {
        this.logger.debug(`"ConciliationBase.tryToConciliateDebits" - Invoice Conciliated! UserCNPJ [${recipientDocument}] | TransmitterCNPJ [${emitterDocument}] | Invoice [${JSON.stringify(invoices)}] | Value [${value}]`);
        await this.conciliateDebits(invoices, debits, false);
      }
    } catch (error) {
      await this.handleError(error, debits);
    }
  }

  protected async tryToConciliateInvoice(startDate: Date, endDate: Date, debits: Debit[], emitterDocument: string, recipientDocument: string): Promise<void> {
    const invoices = await this.innerInvoiceClient.getUnreconciled({
      startDate,
      endDate,
      transmitterCNPJ: emitterDocument,
      userCNPJ: recipientDocument,
    });

    let innerDebits = [...debits];

    for (const invoice of invoices) {
      const combinations = this.innerCombinationsUtil.byValue(innerDebits, +invoice.value);

      if (combinations.length === 1) {
        const [match] = combinations;
        try {
          await this.validateInvoicesIssueDate([invoice]);

          await this.conciliateDebits([invoice], match, false);
        } catch (error) {
          await this.handleError(error, debits);
        }

        innerDebits = innerDebits
          .filter((debit: Debit): boolean => !match.some((el: Debit): boolean => el.id === debit.id));
      }
    }
  }

  protected async conciliateDebits(invoices: Invoice[], debits: Debit[], handleError = true): Promise<void> {
    try {
      await this.innerConciliationClient.conciliate(debits, invoices);
    } catch (error) {
      if (handleError) await this.handleError(error, debits);
      throw error;
    }
  }

  protected async getInvoicesToConciliate(startDate: Date, endDate: Date, emitterDocument: string, recipientDocument: string, value: number): Promise<Invoice[]> {
    const candidates = await this.innerInvoiceClient.getUnreconciled({
      startDate,
      endDate,
      transmitterCNPJ: emitterDocument,
      userCNPJ: recipientDocument,
    });

    if (!candidates?.length) {
      return []; // Cannot conciliate
    }

    const invoices = this.processCandidates(candidates, value);

    await this.validateInvoicesIssueDate(invoices);

    return invoices;
  }

  protected async validateInvoicesIssueDate(invoices: Invoice[]): Promise<void> {
    const notOlderEnough = invoices.filter(({ createdAt }: Invoice): boolean => !this.isOlderThan24Hours(createdAt));

    if (notOlderEnough?.length) {
      const numbers = notOlderEnough.map(({ number }: Invoice): string => number);
      throw new InvoiceInCancellationPeriod(`There is(are) ${numbers.length} invoice(s) that is(are) under the Cancellation Period. [${numbers.join(', ')}]`);
    }
  }

  private isOlderThan24Hours(date: Date): boolean {
    const currentDate = new Date();

    const diff = (currentDate.getTime() - date.getTime()) / 1000 / 60 / 60;

    return diff >= 24;
  }

  private processCandidates(candidates: Invoice[], value: number): Invoice[] {
    const lazyCandidates = candidates.map((el: Invoice) => ({ id: el.id, value: el.value }));
    const invoiceCombinations = this.innerCombinationsUtil.byValue(lazyCandidates, value, true);

    if (!invoiceCombinations?.length) {
      return [];
    }

    if (invoiceCombinations.length > 1) {
      throw new MultipleInvoices(`Found ${invoiceCombinations.length} invoice combinations. User must to do manual conciliation.`);
    }

    return invoiceCombinations[0]
      .map(({ id }): Invoice => candidates.find((el: Invoice): boolean => el.id === id) as Invoice);
  }

  protected async handleError(error: any, debits: Debit[]): Promise<void> {
    await this.innerCreateEustIssueUseCase.execute({
      unit: EustIssueUnit.Conciliation,
      type: error?.code ?? EustIssueType.InternalError,
      debitIds: debits.map(({ id }: Debit): string => id),
      description: error?.message,
      causedBy: error,
      resolved: !!error?.resolved,
    });
  }
}
