import { GraphQLError } from 'graphql';
import type { Resolvers } from '../../../__generated__/types.js';
import { UUID_REGEX } from '../../../shared/constants.js';
import type { CountryCode } from '../../../shared/enums.js';
import { AdminContextProvider } from '../../admin-context/providers/admin-context.provider.js';
import { updateGreenInvoiceClient } from '../../green-invoice/helpers/green-invoice-clients.helper.js';
import { SortCodesProvider } from '../../sort-codes/providers/sort-codes.provider.js';
import { TagsProvider } from '../../tags/providers/tags.provider.js';
import { TransactionsProvider } from '../../transactions/providers/transactions.provider.js';
import {
  SuggestionData,
  suggestionDataSchema,
} from '../helpers/business-suggestion-data-schema.helper.js';
import { updateSuggestions } from '../helpers/businesses.helper.js';
import { hasFinancialEntitiesCoreProperties } from '../helpers/financial-entities.helper.js';
import { filterBusinessByName } from '../helpers/utils.helper.js';
import { BusinessesProvider } from '../providers/businesses.provider.js';
import { FinancialEntitiesProvider } from '../providers/financial-entities.provider.js';
import { TaxCategoriesProvider } from '../providers/tax-categories.provider.js';
import type {
  FinancialEntitiesModule,
  IGetBusinessesByIdsResult,
  IUpdateBusinessParams,
  IUpdateBusinessTaxCategoryParams,
} from '../types.js';
import { commonChargeFields, commonFinancialEntityFields } from './common.js';

export const businessesResolvers: FinancialEntitiesModule.Resolvers &
  Pick<Resolvers, 'Business' | 'UpdateBusinessResponse'> = {
  Query: {
    business: async (_, { id }, { injector }) => {
      const dbFe = await injector.get(BusinessesProvider).getBusinessByIdLoader.load(id);
      if (!dbFe) {
        throw new Error(`Business ID="${id}" not found`);
      }

      return dbFe;
    },
    businesses: async (_, { ids }, { injector }) => {
      const businesses = await injector.get(BusinessesProvider).getBusinessByIdLoader.loadMany(ids);
      if (businesses.some(business => !business || business instanceof Error)) {
        throw new Error(`Error fetching some of the businesses`);
      }

      return businesses as IGetBusinessesByIdsResult[];
    },
    allBusinesses: async (_, { page, limit, name }, { injector }) => {
      const businesses = await injector.get(BusinessesProvider).getAllBusinesses();

      const filteredBusinesses = businesses.filter(business =>
        filterBusinessByName(business, name),
      );

      page ??= 0;
      let pageBusinesses = filteredBusinesses.sort((a, b) =>
        a.name.toLowerCase() > b.name.toLowerCase() ? 1 : -1,
      );

      // handle pagination
      if (limit) {
        pageBusinesses = filteredBusinesses.slice(page * limit, (page + 1) * limit);
      }

      return {
        __typename: 'PaginatedBusinesses',
        nodes: pageBusinesses,
        pageInfo: {
          totalPages: limit ? Math.ceil(filteredBusinesses.length / limit) : 1,
          totalRecords: filteredBusinesses.length,
          currentPage: page,
          pageSize: limit,
        },
      };
    },
  },
  Mutation: {
    updateBusiness: async (_, { businessId, ownerId, fields }, { injector }) => {
      if (hasFinancialEntitiesCoreProperties(fields)) {
        await injector.get(FinancialEntitiesProvider).updateFinancialEntity({
          ...fields,
          financialEntityId: businessId,
          irsCode: fields.irsCode ?? null,
          isActive: fields.isActive ?? null,
        });
      }

      let suggestionData: SuggestionData | null = null;
      if (fields.suggestions) {
        const currentBusiness = await injector
          .get(BusinessesProvider)
          .getBusinessByIdLoader.load(businessId);
        if (!currentBusiness) {
          return {
            __typename: 'CommonError',
            message: `Business ID="${businessId}" not found`,
          };
        }
        suggestionData = updateSuggestions(fields.suggestions, currentBusiness.suggestion_data);
      }
      const adjustedFields: IUpdateBusinessParams = {
        address: fields.address,
        city: fields.city,
        zipCode: fields.zipCode,
        email: fields.email,
        exemptDealer: fields.exemptDealer,
        vatNumber: fields.governmentId,
        hebrewName: fields.hebrewName,
        phoneNumber: fields.phoneNumber,
        website: fields.website,
        optionalVat: fields.optionalVAT,
        isReceiptEnough: fields.isReceiptEnough ?? null,
        isDocumentsOptional: fields.isDocumentsOptional ?? null,
        country: fields.country,
        suggestionData,
        pcn874RecordTypeOverride: fields.pcn874RecordType,
      };

      let updatedBusiness: IGetBusinessesByIdsResult | undefined;

      try {
        if (Object.values(adjustedFields).some(field => field != null)) {
          await injector
            .get(BusinessesProvider)
            .updateBusiness({ ...adjustedFields, businessId })
            .catch((e: Error) => {
              const message = `Error updating business ID="${businessId}"`;
              console.error(`${message}: ${e}`);
              if (e instanceof GraphQLError) {
                throw e;
              }
              throw new Error(message);
            });
        }

        if (fields.taxCategory) {
          const texCategoryParams: IUpdateBusinessTaxCategoryParams = {
            businessId,
            ownerId,
            taxCategoryId: fields.taxCategory,
          };
          try {
            await injector.get(TaxCategoriesProvider).insertBusinessTaxCategory(texCategoryParams);
          } catch (error) {
            console.error(error);
            await injector
              .get(TaxCategoriesProvider)
              .updateBusinessTaxCategory(texCategoryParams)
              .catch((e: Error) => {
                const message = `Error updating tax category for business ID="${businessId}"`;
                console.error(`${message}: ${e}`);
                throw new Error(message);
              });
          }
        }

        updatedBusiness = await injector
          .get(BusinessesProvider)
          .getBusinessByIdLoader.load(businessId);
        if (!updatedBusiness) {
          throw new Error(`Updated business not found`);
        }
      } catch (e) {
        return {
          __typename: 'CommonError',
          message: `Failed to update business ID="${businessId}": ${(e as Error).message}`,
        };
      }

      // update green invoice client if needed
      await updateGreenInvoiceClient(businessId, injector, fields);

      return updatedBusiness;
    },
    insertNewBusiness: async (_, { fields }, { injector }) => {
      try {
        if (!fields.country) {
          throw new GraphQLError(`Country is required to create a new business`);
        }

        const { ownerId } = await injector.get(AdminContextProvider).getVerifiedAdminContext();

        let irsCode = fields.irsCode ?? null;
        if (!irsCode && fields.sortCode) {
          const sortCode = await injector
            .get(SortCodesProvider)
            .getSortCodesByIdLoader.load({ key: fields.sortCode, ownerId });
          if (sortCode?.default_irs_code) {
            irsCode = sortCode.default_irs_code;
          }
        }
        const [financialEntity] = await injector
          .get(FinancialEntitiesProvider)
          .insertFinancialEntity({
            ownerId,
            name: fields.name,
            sortCode: fields.sortCode,
            type: 'business',
            irsCode,
            isActive: fields.isActive ?? true,
          })
          .catch((e: Error) => {
            console.error(e);
            return [];
          });

        if (!financialEntity) {
          return {
            __typename: 'CommonError',
            message: `Failed to create core financial entity`,
          };
        }

        const suggestions: SuggestionData | undefined = fields.suggestions
          ? {
              tags: fields.suggestions.tags?.map(tag => tag.id),
              phrases: fields.suggestions.phrases?.map(phrase => phrase),
              description: fields.suggestions.description ?? undefined,
              emails: fields.suggestions.emails ? [...fields.suggestions.emails] : undefined,
              emailListener: fields.suggestions.emailListener
                ? {
                    ...fields.suggestions.emailListener,
                    internalEmailLinks: fields.suggestions.emailListener.internalEmailLinks
                      ? [...fields.suggestions.emailListener.internalEmailLinks]
                      : undefined,
                    emailBody: fields.suggestions.emailListener.emailBody ?? undefined,
                    attachments: fields.suggestions.emailListener.attachments
                      ? [...fields.suggestions.emailListener.attachments]
                      : undefined,
                  }
                : undefined,
              priority: fields.suggestions.priority ?? undefined,
            }
          : undefined;

        const business = await injector.get(BusinessesProvider).insertBusinessLoader.load({
          id: financialEntity.id,
          ownerId: financialEntity.owner_id,
          address: fields.address,
          city: fields.city,
          zipCode: fields.zipCode,
          email: fields.email,
          exemptDealer: fields.exemptDealer ?? false,
          governmentId: fields.governmentId,
          hebrewName: fields.hebrewName,
          phoneNumber: fields.phoneNumber,
          website: fields.website,
          optionalVat: fields.optionalVAT ?? false,
          isReceiptEnough: fields.isReceiptEnough ?? false,
          isDocumentsOptional: fields.isDocumentsOptional ?? false,
          country: fields.country,
          suggestions,
          pcn874RecordTypeOverride: fields.pcn874RecordType,
        });

        if (!business) {
          throw new Error(`Failed to create core business`);
        }

        if (fields.taxCategory) {
          const taxCategoryParams: IUpdateBusinessTaxCategoryParams = {
            businessId: financialEntity.id,
            ownerId,
            taxCategoryId: fields.taxCategory,
          };
          await injector
            .get(TaxCategoriesProvider)
            .insertBusinessTaxCategory(taxCategoryParams)
            .then(res => res[0])
            .catch((e: Error) => {
              const message = `Error updating tax category for business ID="${financialEntity.id}"`;
              console.error(`${message}: ${e}`);
              throw new Error(message);
            });
        }

        return {
          ...financialEntity,
          ...business,
        };
      } catch (e) {
        console.error(e);
        return {
          __typename: 'CommonError',
          message: `Failed to create business`,
        };
      }
    },
    mergeBusinesses: async (_, { targetBusinessId, businessIdsToMerge }, { injector }) => {
      try {
        const [targetBusiness, ...businessesToMerge] = await injector
          .get(BusinessesProvider)
          .getBusinessByIdLoader.loadMany([targetBusinessId, ...businessIdsToMerge]);
        if (!targetBusiness || targetBusiness instanceof Error) {
          throw new GraphQLError(`Target business not found`);
        }

        const mergeBusinessPromises = businessIdsToMerge.map(businessIdToMerge =>
          injector
            .get(FinancialEntitiesProvider)
            .replaceFinancialEntity(targetBusinessId, businessIdToMerge, true),
        );
        await Promise.all(mergeBusinessPromises);

        // handle suggestions
        let suggestionData: SuggestionData | null = null;

        const {
          data: targetSuggestionData,
          error,
          success,
        } = suggestionDataSchema.safeParse(targetBusiness.suggestion_data);
        if (success) {
          suggestionData = targetSuggestionData;
        } else {
          console.error('Failed to parse suggestion data for business', {
            businessId: targetBusiness.id,
            error,
          });
          throw new GraphQLError('Failed to parse suggestion data for target business');
        }

        for (const businessToMerge of businessesToMerge) {
          if (!businessToMerge || businessToMerge instanceof Error) {
            continue;
          }
          const { data: mergedBusinessSuggestionData, success } = suggestionDataSchema.safeParse(
            businessToMerge.suggestion_data,
          );
          if (success && mergedBusinessSuggestionData.phrases) {
            suggestionData = updateSuggestions(
              { phrases: mergedBusinessSuggestionData.phrases },
              suggestionData,
              true,
            );
          }
        }
        if (suggestionData) {
          await injector.get(BusinessesProvider).updateBusiness({
            businessId: targetBusinessId,
            suggestionData,
          });
        }

        // refetch updated target business
        const updatedBusiness = await injector
          .get(BusinessesProvider)
          .getBusinessByIdLoader.load(targetBusinessId);
        if (!updatedBusiness || updatedBusiness instanceof Error) {
          throw new GraphQLError(`Target business not found`);
        }
        return updatedBusiness;
      } catch (e) {
        console.error(e);
        if (e instanceof GraphQLError) {
          throw e;
        }
        throw new GraphQLError(`Failed to merge businesses`);
      }
    },
    batchGenerateBusinessesOutOfTransactions: async (_, __, { injector }) => {
      const { ownerId, locality } = await injector
        .get(AdminContextProvider)
        .getVerifiedAdminContext();
      const transactionsPromise = injector.get(TransactionsProvider).getTransactionsByFilters({
        ownerIDs: [ownerId],
      });
      const businessesPromise = injector.get(BusinessesProvider).getAllBusinesses();
      const [transactions, businesses] = await Promise.all([
        transactionsPromise,
        businessesPromise,
      ]);

      const businessIdsPerDescription = new Map<string, Set<string>>();
      transactions.map(transaction => {
        if (!transaction.source_description) {
          return;
        }
        const description = transaction.source_description.toLowerCase();
        if (!businessIdsPerDescription.has(description)) {
          businessIdsPerDescription.set(description, new Set());
        }
        if (transaction.business_id) {
          businessIdsPerDescription.get(description)?.add(transaction.business_id);
        }
      });
      businesses.map(business => {
        const { data: suggestionData, success } = suggestionDataSchema.safeParse(
          business.suggestion_data,
        );
        if (!success || !suggestionData.phrases) {
          return;
        }
        suggestionData.phrases.map(phrase => {
          const description = phrase.toLowerCase();
          if (!businessIdsPerDescription.has(description)) {
            businessIdsPerDescription.set(description, new Set());
          }
          businessIdsPerDescription.get(description)?.add(business.id);
        });
      });

      const descriptionBusinessPairs: [string, string][] = [];
      const multipleMatchesDescriptions: [string, string[]][] = [];
      const nonMatchesDescriptions: string[] = [];

      Array.from(businessIdsPerDescription.entries()).map(async ([description, businessIds]) => {
        if (businessIds.size === 1) {
          descriptionBusinessPairs.push([description, businessIds.values().next().value!]);
          return;
        }

        if (businessIds.size > 1) {
          multipleMatchesDescriptions.push([description, Array.from(businessIds.values())]);
          return;
        }

        nonMatchesDescriptions.push(description);
      });

      console.log(
        'Multiple matches descriptions:',
        multipleMatchesDescriptions
          .map(([description, businessIDs]) => `\n  "${description}": ${businessIDs.join(', ')}`)
          .join(''),
      );

      const uniqueDescriptions = Array.from(new Set<string>(nonMatchesDescriptions)).sort();

      const businessTransactions = new Map<string, string[]>();

      transactions.map(t => {
        if (!t.business_id && t.source_description) {
          const businessId = descriptionBusinessPairs.find(
            pair => pair[0] === t.source_description,
          );
          if (businessId) {
            if (!businessTransactions.has(businessId[1])) {
              businessTransactions.set(businessId[1], []);
            }
            businessTransactions.get(businessId[1])?.push(t.id);
          }
        }
      });

      Array.from(businessTransactions.entries())
        .map(([businessId, transactionIds]) =>
          transactionIds.map(transactionId =>
            injector.get(TransactionsProvider).updateTransactions({
              businessId,
              transactionIds: [transactionId],
            }),
          ),
        )
        .flat();

      const newBusinessesIds = await Promise.all(
        uniqueDescriptions.map(async description => {
          const financialEntity = await injector
            .get(FinancialEntitiesProvider)
            .insertFinancialEntitiesLoader.load({
              ownerId,
              name: description,
              type: 'business',
              sortCode: null,
              irsCode: null,
              isActive: true,
            })
            .catch((e: Error) => {
              console.error(e);
              return null;
            });

          if (!financialEntity) {
            throw new Error(
              `Failed to create core financial entity for description "${description}"`,
            );
          }

          const business = await injector.get(BusinessesProvider).insertBusinessLoader.load({
            id: financialEntity.id,
            ownerId: financialEntity.owner_id,
            country: locality,
            hebrewName: description,
            suggestions: {
              phrases: [description],
            },
            address: undefined,
            city: undefined,
            zipCode: undefined,
            email: undefined,
            website: undefined,
            phoneNumber: undefined,
            governmentId: undefined,
            exemptDealer: false,
            optionalVat: false,
            isReceiptEnough: false,
            isDocumentsOptional: false,
            pcn874RecordTypeOverride: null,
          });

          if (!business) {
            throw new Error(`Failed to create business for description "${description}"`);
          }

          return business.id;
        }),
      );

      return injector
        .get(BusinessesProvider)
        .getBusinessByIdLoader.loadMany(newBusinessesIds) as Promise<IGetBusinessesByIdsResult[]>;
    },
  },
  Business: {
    __resolveType: async (
      parent,
      { injector },
    ): Promise<'LtdFinancialEntity' | 'PersonalFinancialEntity' | null> => {
      if (!parent.country) {
        const business = await injector
          .get(BusinessesProvider)
          .getBusinessByIdLoader.load(parent.id);
        if (business) {
          Object.assign(parent, business);
        }
      }
      return 'LtdFinancialEntity';
    },
  },
  LtdFinancialEntity: {
    ...commonFinancialEntityFields,
    country: DbBusiness => DbBusiness.country as CountryCode,
    governmentId: DbBusiness => DbBusiness.vat_number,
    address: DbBusiness => DbBusiness.address ?? DbBusiness.address_hebrew,
    city: DbBusiness => DbBusiness.city,
    zipCode: DbBusiness => DbBusiness.zip_code,

    hebrewName: DbBusiness => DbBusiness.hebrew_name,
    email: DbBusiness => DbBusiness.email,
    exemptDealer: DbBusiness => DbBusiness.exempt_dealer,
    website: DbBusiness => DbBusiness.website,
    phoneNumber: DbBusiness => DbBusiness.phone_number,
    suggestions: (DbBusiness, _, { injector }) => {
      const { data: suggestionData, success } = suggestionDataSchema.safeParse(
        DbBusiness.suggestion_data,
      );
      if (success) {
        return {
          tags: suggestionData.tags
            ? suggestionData.tags.map(suggestedTag =>
                (UUID_REGEX.test(suggestedTag)
                  ? injector.get(TagsProvider).getTagByIDLoader.load(suggestedTag)
                  : injector.get(TagsProvider).getTagByNameLoader.load(suggestedTag)
                )
                  .then(tag => {
                    if (!tag) {
                      throw new GraphQLError(`Tag "${suggestedTag}" not found`);
                    }
                    return tag;
                  })
                  .catch(e => {
                    console.error(e);
                    throw new GraphQLError(`Failed to load tag "${suggestedTag}"`);
                  }),
              )
            : [],
          phrases: suggestionData.phrases ?? [],
          description: suggestionData.description ?? null,
          emails: suggestionData.emails ?? [],
          emailListener: suggestionData.emailListener ?? null,
          priority: suggestionData.priority ?? null,
        };
      }
      return null;
    },
    optionalVAT: DbBusiness => DbBusiness.optional_vat,
    pcn874RecordType: DbBusiness => DbBusiness.pcn874_record_type_override,
    isReceiptEnough: DbBusiness => DbBusiness.can_settle_with_receipt,
    isDocumentsOptional: DbBusiness => DbBusiness.no_invoices_required,
  },
  PersonalFinancialEntity: {
    ...commonFinancialEntityFields,
    email: DbBusiness => DbBusiness.email ?? '', // TODO: remove alternative ''
    pcn874RecordType: DbBusiness => DbBusiness.pcn874_record_type_override,
  },
  UpdateBusinessResponse: {
    __resolveType: (obj, _context, _info) => {
      if ('__typename' in obj && obj.__typename === 'CommonError') return 'CommonError';
      return 'LtdFinancialEntity';
    },
  },
  CommonCharge: commonChargeFields,
  FinancialCharge: commonChargeFields,
  ConversionCharge: commonChargeFields,
  SalaryCharge: commonChargeFields,
  InternalTransferCharge: commonChargeFields,
  DividendCharge: commonChargeFields,
  BusinessTripCharge: commonChargeFields,
  MonthlyVatCharge: commonChargeFields,
  BankDepositCharge: commonChargeFields,
  ForeignSecuritiesCharge: commonChargeFields,
  CreditcardBankCharge: commonChargeFields,
};
