/* eslint-disable no-unused-vars */
/* eslint-disable no-restricted-syntax */
/* eslint-disable no-await-in-loop */
import { Inject, Injectable } from '@severo-tech/injection-decorator';
import { ConciliationBase } from './conciliation-base';
import { PromisesUtil } from '../../../utils';
import { Debit } from '../../../../domain/debit';
import { AgentType } from '../../../../domain/agent';
import { IAgentClient, IDebitClient, IInvoiceClient } from '../../../contracts';
import { IConfig } from '../../../contracts/config';

interface IGroupedDebit {
  emitterDocument: string;
  recipientDocument: string;
  value: number;
  debits: Debit[];
}

@Injectable()
export class ConciliationManyToMany extends ConciliationBase {
  @Inject('Agent01EUSTClient')
  private readonly agentClient!: IAgentClient;

  @Inject('Debit01EUSTClient')
  private readonly debitClient!: IDebitClient;

  @Inject('PromisesUtil')
  private readonly promisesUtil!: PromisesUtil;

  @Inject('EnvConfig')
  private readonly config!: IConfig;

  public async execute(year: number, month: number, startDate: Date, endDate: Date): Promise<void> {
    this.logger.info('"ConciliationManyToMany.execute" - Conciliating Many Users to Many Transmitters debits');

    await this.executeBy((debits) => this.groupDebitsByUserCNPJAndTransmitterCNPJ(debits), year, month, startDate, endDate);

    await this.executeBy((debits) => this.groupDebitsByUserCNPJAndTransmitterRoot(debits), year, month, startDate, endDate);

    await this.executeBy((debits) => this.groupDebitsByUserRootAndTransmitterCNPJ(debits), year, month, startDate, endDate);

    await this.executeBy((debits) => this.groupDebitsByUserRootAndTransmitterRoot(debits), year, month, startDate, endDate);

    await this.executeByInvoice(year, month, startDate, endDate);

    this.logger.info('"ConciliationManyToMany.execute" - Finished Many-To-Many debits conciliation');
  }

  public async executeBy(groupMethod: (debits: Debit[]) => Promise<IGroupedDebit[]>, year: number, month: number, startDate: Date, endDate: Date): Promise<void> {
    const debits = await this.debitClient.find({
      year,
      month,
      reconciled: false,
    });
    const groups = await groupMethod(debits);

    this.logger.info(`"ConciliationManyToMany.execute" - Grouped ${groups.length} Debits to process`);

    const { maxParallelExecution } = this.config.service;
    await this.promisesUtil.groupExecute(groups, maxParallelExecution, async (group: IGroupedDebit): Promise<void> => {
      await this.tryToConciliateDebits(startDate, endDate, group.debits, group.emitterDocument, group.recipientDocument, group.value);
    });
  }

  private async executeByInvoice(year: number, month: number, startDate: Date, endDate: Date): Promise<void> {
    const debits = await this.debitClient.find({
      year,
      month,
      reconciled: false,
    });
    const groupedDebits = await this.groupDebitsByUserRootAndTransmitterRoot(debits);

    this.logger.info(`"ConciliationManyToMany.executeByInvoice" - Grouped ${groupedDebits.length} Debits to process`);

    const { maxParallelExecution } = this.config.service;
    await this.promisesUtil.groupExecute(groupedDebits, maxParallelExecution, async (group: IGroupedDebit): Promise<void> => {
      await this.tryToConciliateInvoice(startDate, endDate, group.debits, group.emitterDocument, group.recipientDocument);
    });
  }

  private async groupDebitsByUserCNPJAndTransmitterCNPJ(debits: Debit[]): Promise<IGroupedDebit[]> {
    let debitsMap = new Map<string, IGroupedDebit>();

    for (const debit of debits) {
      if (debit.value > 0) {
        const [transmitter] = await this.agentClient.find({ code: debit.transmitterCode, type: AgentType.Transmitter });
        if (transmitter) {
          const [agent] = await this.agentClient.find({ code: debit.userCode, type: AgentType.User });
          debitsMap = this.setGroupMap(debitsMap, transmitter.cnpj, agent.cnpj, debit);
        }
      }
    }
    return [...debitsMap.values()].filter((el: IGroupedDebit): boolean => el.debits.length > 1);
  }

  private async groupDebitsByUserCNPJAndTransmitterRoot(debits: Debit[]): Promise<IGroupedDebit[]> {
    let debitsMap = new Map<string, IGroupedDebit>();

    for (const debit of debits) {
      if (debit.value > 0) {
        const [transmitter] = await this.agentClient.find({ code: debit.transmitterCode, type: AgentType.Transmitter });
        if (transmitter) {
          const [agent] = await this.agentClient.find({ code: debit.userCode, type: AgentType.User });
          const recipientDocRoot = agent.cnpj.substring(0, 8);
          debitsMap = this.setGroupMap(debitsMap, transmitter.cnpj, recipientDocRoot, debit);
        }
      }
    }
    return [...debitsMap.values()].filter((el: IGroupedDebit): boolean => el.debits.length > 1);
  }

  private async groupDebitsByUserRootAndTransmitterCNPJ(debits: Debit[]): Promise<IGroupedDebit[]> {
    let debitsMap = new Map<string, IGroupedDebit>();

    for (const debit of debits) {
      if (debit.value > 0) {
        const [transmitter] = await this.agentClient.find({ code: debit.transmitterCode, type: AgentType.Transmitter });
        if (transmitter) {
          const [agent] = await this.agentClient.find({ code: debit.userCode, type: AgentType.User });
          const emitterDocRoot = transmitter.cnpj.substring(0, 8);
          debitsMap = this.setGroupMap(debitsMap, emitterDocRoot, agent.cnpj, debit);
        }
      }
    }
    return [...debitsMap.values()].filter((el: IGroupedDebit): boolean => el.debits.length > 1);
  }

  private async groupDebitsByUserRootAndTransmitterRoot(debits: Debit[]): Promise<IGroupedDebit[]> {
    let debitsMap = new Map<string, IGroupedDebit>();

    for (const debit of debits) {
      if (debit.value > 0) {
        const [transmitter] = await this.agentClient.find({ code: debit.transmitterCode, type: AgentType.Transmitter });
        if (transmitter) {
          const [agent] = await this.agentClient.find({ code: debit.userCode, type: AgentType.User });
          const recipientDocRoot = agent.cnpj.substring(0, 8);
          const emitterDocRoot = transmitter.cnpj.substring(0, 8);
          debitsMap = this.setGroupMap(debitsMap, emitterDocRoot, recipientDocRoot, debit);
        }
      }
    }
    return [...debitsMap.values()].filter((el: IGroupedDebit): boolean => el.debits.length > 1);
  }

  private setGroupMap(groupMap: Map<string, IGroupedDebit>, emitterDocument: string, recipientDocument: string, debit: Debit): Map<string, IGroupedDebit> {
    const mapKey = `${recipientDocument}-${emitterDocument}`;

    const existingGrouped = groupMap.get(mapKey);
    return groupMap.set(mapKey, {
      emitterDocument,
      recipientDocument,
      debits: [...(existingGrouped?.debits ?? []), debit],
      value: +((existingGrouped?.value ?? 0) + debit.value).toFixed(2),
    });
  }
}
