import { GraphQLError } from 'graphql';
import { mergeChargesExecutor } from '@modules/charges/helpers/merge-charges.hepler.js';
import { ChargesProvider } from '@modules/charges/providers/charges.provider.js';
import type { IGetChargesByIdsResult } from '@modules/charges/types.js';
import { TransactionsProvider } from '@modules/transactions/providers/transactions.provider.js';
import type { IGetTransactionsByChargeIdsResult } from '@modules/transactions/types.js';
import { DEFAULT_FINANCIAL_ENTITY_ID } from '@shared/constants';
import { CornJobsProvider } from '../providers/corn-jobs.provider.js';
import type { CornJobsModule } from '../types.js';

const WIDE_DATE_DIFF_MILLISECONDS = 2_592_000_000; // 30 days
const ACCEPTABLE_DATE_DIFF_MILLISECONDS = 86_400_000; // 1 days

export const cornJobsResolvers: CornJobsModule.Resolvers = {
  Mutation: {
    mergeChargesByTransactionReference: async (_, __, { injector }) => {
      try {
        const candidates = (await injector.get(CornJobsProvider).getReferenceMergeCandidates({
          ownerId: DEFAULT_FINANCIAL_ENTITY_ID,
        })) as IGetTransactionsByChargeIdsResult[];

        const chargeIds = new Set<string>(candidates.map(candidate => candidate.charge_id!));
        const charges = await injector
          .get(ChargesProvider)
          .getChargeByIdLoader.loadMany(Array.from(chargeIds))
          .then(res => res.filter(charge => charge && 'id' in charge) as IGetChargesByIdsResult[]);

        const referenceMap = new Map<
          string,
          { transaction: IGetTransactionsByChargeIdsResult; charge: IGetChargesByIdsResult }[]
        >();
        candidates.map(candidate => {
          const reference = candidate.source_reference;
          if (!reference) {
            throw new GraphQLError('reference is missing');
          }

          const charge = charges.find(charge => charge.id === candidate.charge_id);
          if (!charge) {
            throw new GraphQLError(`Charge is missing for transaction ID=${candidate.id}`);
          }

          if (!referenceMap.has(reference)) {
            referenceMap.set(reference, []);
          }
          referenceMap.get(reference)?.push({ transaction: candidate, charge });
        });

        const mergableMathces: Record<string, string[]> = {};
        Array.from(referenceMap.values()).map(candidates => {
          const candidatesIDs = new Set<string>(
            candidates.map(candidate => candidate.transaction.id!),
          );
          for (const { transaction, charge } of candidates) {
            if (!transaction.id || !candidatesIDs.has(transaction.id) || !transaction.event_date) {
              continue;
            }
            candidatesIDs.delete(transaction.id);

            // filter candidates
            const fromDate = transaction.event_date.getTime() - ACCEPTABLE_DATE_DIFF_MILLISECONDS;
            const toDate = transaction.event_date.getTime() + ACCEPTABLE_DATE_DIFF_MILLISECONDS;
            const widelyFromDate = transaction.event_date.getTime() - WIDE_DATE_DIFF_MILLISECONDS;
            const widelyToDate = transaction.event_date.getTime() + WIDE_DATE_DIFF_MILLISECONDS;

            const matches = candidates.filter(({ transaction: internatTransaction }) => {
              if (!candidatesIDs.has(internatTransaction.id)) {
                return false;
              }

              const isWithinTimeRange =
                internatTransaction.event_date.getTime() >= fromDate &&
                internatTransaction.event_date.getTime() <= toDate;

              // check if details match
              const isWidelyInTimeRange =
                internatTransaction.event_date.getTime() >= widelyFromDate &&
                internatTransaction.event_date.getTime() <= widelyToDate;
              const areDetailsSufficient =
                isWidelyInTimeRange &&
                internatTransaction.source_details &&
                internatTransaction.source_details.length >= 5 &&
                transaction.source_details &&
                transaction.source_details.length >= 5;
              const isMatchingDetails =
                areDetailsSufficient &&
                (internatTransaction.source_details?.includes(transaction.source_details!) ||
                  internatTransaction.source_details?.includes(transaction.source_details!));

              return isWithinTimeRange || isMatchingDetails;
            });

            if (matches.length === 0) {
              return;
            }
            matches.map(match => candidatesIDs.delete(match.transaction.id!));
            matches.push({ transaction, charge });

            // preparation for merge: rearrange based on charge
            const chargesWithTransactions = new Map<
              string,
              { transactions: IGetTransactionsByChargeIdsResult[]; charge: IGetChargesByIdsResult }
            >();
            matches.map(({ transaction, charge }) => {
              if (!chargesWithTransactions.has(charge.id)) {
                chargesWithTransactions.set(charge.id, { transactions: [], charge });
              }
              chargesWithTransactions.get(charge.id)?.transactions.push(transaction);
            });
            if (chargesWithTransactions.size === 1) {
              return;
            }

            // figure which charge will be the main for merge
            const chargeMatches = Array.from(chargesWithTransactions.values());
            const mainCandidates = chargeMatches.filter(({ charge, transactions }) => {
              const isFee = !transactions.some(transaction => !transaction.is_fee);
              const isUnlinked = !!charge.user_description?.includes('unlinked from charge');
              return !isFee && !isUnlinked;
            });

            if (mainCandidates.length === 0) {
              const main = chargeMatches.shift();
              logMatch(main!, chargeMatches);
              mergableMathces[main!.charge.id] = chargeMatches.map(match => match.charge.id);
              return;
            }

            if (mainCandidates.length === 1) {
              const main = mainCandidates[0];
              const chargesToMerge = chargeMatches.filter(
                match => match.charge.id !== main.charge.id,
              );
              logMatch(main, chargesToMerge);
              mergableMathces[main.charge.id] = chargesToMerge.map(match => match.charge.id);
              return;
            }

            const main = chargeMatches.shift();
            logMatch(main!, chargeMatches);
            console.log('not sure what to do now');
          }
        });

        // execute merges
        const chargeMergePromises = Object.entries(mergableMathces).map(
          async ([baseChargeID, chargeIdsToMerge]) => {
            await mergeChargesExecutor(chargeIdsToMerge, baseChargeID, injector);
            console.log(`Merged into charge ID=${baseChargeID} successfully`);
          },
        );
        await Promise.all(chargeMergePromises);

        return {
          success: true,
          charges: Object.keys(mergableMathces)
            .map(id => charges.find(charge => charge.id === id))
            .filter(charge => charge) as IGetChargesByIdsResult[],
        };
      } catch (e) {
        return {
          success: false,
          errors: [(e as Error)?.message ?? 'Unknown error'],
        };
      }
    },
    flagForeignFeeTransactions: async (_, __, { injector }) => {
      try {
        const updatedTransactionsId = await injector
          .get(CornJobsProvider)
          .flagForeignFeeTransactions({ ownerId: DEFAULT_FINANCIAL_ENTITY_ID });
        const res = await injector
          .get(TransactionsProvider)
          .getTransactionByIdLoader.loadMany(updatedTransactionsId.map(({ id }) => id));
        return {
          success: true,
          transactions: res.filter(
            transaction => transaction,
          ) as IGetTransactionsByChargeIdsResult[],
        };
      } catch (e) {
        return {
          success: false,
          errors: [(e as Error)?.message ?? 'Unknown error'],
        };
      }
    },
  },
};

function logMatch(
  main: { transactions: IGetTransactionsByChargeIdsResult[]; charge: IGetChargesByIdsResult },
  others: { transactions: IGetTransactionsByChargeIdsResult[]; charge: IGetChargesByIdsResult }[],
) {
  console.log('\n\n\n');
  for (const { charge, transactions } of [main, ...others]) {
    console.log(charge.user_description);
    for (const transaction of transactions) {
      console.log('  ', transaction.source_description);
    }
  }
}
