import { endOfMonth, format, startOfMonth, subMonths } from 'date-fns';
import { GraphQLError } from 'graphql';
import {
  DocumentInput_Input,
  Currency as GreenInvoiceCurrency,
} from '@accounter/green-invoice-graphql';
import { CloudinaryProvider } from '@modules/app-providers/cloudinary.js';
import { GoogleDriveProvider } from '@modules/app-providers/google-drive/google-drive.provider.js';
import { GreenInvoiceProvider } from '@modules/app-providers/green-invoice.js';
import { deleteCharges } from '@modules/charges/helpers/delete-charges.helper.js';
import { ChargesProvider } from '@modules/charges/providers/charges.provider.js';
import { BusinessesGreenInvoiceMatcherProvider } from '@modules/financial-entities/providers/businesses-green-invoice-match.provider.js';
import { BusinessesProvider } from '@modules/financial-entities/providers/businesses.provider.js';
import { EMPTY_UUID } from '@shared/constants';
import { Currency, DocumentType } from '@shared/enums';
import { Resolvers } from '@shared/gql-types';
import {
  dateToTimelessDateString,
  formatCurrency,
  optionalDateToTimelessDateString,
} from '@shared/helpers';
import {
  getGreenInvoiceDocumentType,
  normalizeDocumentType,
} from '../helpers/green-invoice.helper.js';
import { getDocumentFromFile } from '../helpers/upload.helper.js';
import { DocumentsProvider } from '../providers/documents.provider.js';
import type {
  DocumentsModule,
  IGetAllDocumentsResult,
  IInsertDocumentsParams,
  IInsertDocumentsResult,
  IUpdateDocumentParams,
} from '../types.js';
import {
  commonChargeFields,
  commonDocumentsFields,
  commonFinancialDocumentsFields,
} from './common.js';

export const documentsResolvers: DocumentsModule.Resolvers &
  Pick<
    Resolvers,
    'UpdateDocumentResult' | 'InsertDocumentResult' | 'UploadDocumentResult' | 'Document'
  > = {
  Query: {
    documents: async (_, __, { injector }) => {
      const dbDocs = await injector.get(DocumentsProvider).getAllDocuments();
      return dbDocs;
    },
    documentsByFilters: async (_, { filters }, { injector }) => {
      const dbDocs = await injector.get(DocumentsProvider).getDocumentsByExtendedFilters(filters);
      return dbDocs;
    },
    documentById: async (_, { documentId }, { injector }) => {
      const doc = await injector.get(DocumentsProvider).getDocumentsByIdLoader.load(documentId);
      return doc ?? null;
    },
  },
  Mutation: {
    uploadDocument: async (_, { file, chargeId }, context) => {
      const { injector } = context;

      try {
        const newDocument = await getDocumentFromFile(context, file, chargeId);

        const res = await injector
          .get(DocumentsProvider)
          .insertDocuments({ document: [{ ...newDocument }] });

        return { document: res[0] as IGetAllDocumentsResult };
      } catch (e) {
        const message = (e as Error)?.message ?? 'Unknown error';
        return {
          __typename: 'CommonError',
          message: `Error uploading document: ${message}`,
        };
      }
    },
    batchUploadDocuments: async (_, { documents, isSensitive, chargeId }, context) => {
      const {
        injector,
        adminContext: { defaultAdminBusinessId },
      } = context;

      if (!chargeId) {
        // generate new charge
        const [newCharge] = await injector.get(ChargesProvider).generateCharge({
          ownerId: defaultAdminBusinessId,
          userDescription: 'New uploaded documents',
        });
        if (!newCharge) {
          throw new GraphQLError(`Failed to generate new charge for new document`);
        }
        chargeId = newCharge.id;
      }

      const newDocuments: Array<IInsertDocumentsParams['document'][number]> = [];
      await Promise.all(
        documents.map(async document => {
          // get new document data
          const newDocument = await getDocumentFromFile(context, document, chargeId, isSensitive);
          newDocuments.push(newDocument);
        }),
      );

      const res = await injector.get(DocumentsProvider).insertDocuments({ document: newDocuments });
      return res.map(document => ({ document: document as IGetAllDocumentsResult }));
    },
    batchUploadDocumentsFromGoogleDrive: async (
      _,
      { sharedFolderUrl, chargeId, isSensitive },
      context,
    ) => {
      const isValidGoogleDriveUrl = (url: string): boolean => {
        try {
          const parsedUrl = new URL(url);
          return (
            parsedUrl.hostname === 'drive.google.com' && parsedUrl.pathname.includes('/folders/')
          );
        } catch {
          return false;
        }
      };

      if (!isValidGoogleDriveUrl(sharedFolderUrl)) {
        throw new GraphQLError('Invalid Google Drive folder URL');
      }

      const {
        injector,
        adminContext: { defaultAdminBusinessId },
      } = context;

      const files = await injector
        .get(GoogleDriveProvider)
        .fetchFilesFromSharedFolder(sharedFolderUrl);

      if (!chargeId) {
        // generate new charge
        const [newCharge] = await injector.get(ChargesProvider).generateCharge({
          ownerId: defaultAdminBusinessId,
          userDescription: 'New uploaded documents',
        });
        if (!newCharge) {
          throw new GraphQLError(`Failed to generate new charge for new document`);
        }
        chargeId = newCharge.id;
      }

      const newDocuments: Array<IInsertDocumentsParams['document'][number]> = [];
      await Promise.all(
        files.map(async file => {
          // get new document data
          try {
            const newDocument = await getDocumentFromFile(context, file, chargeId, isSensitive);
            newDocuments.push(newDocument);
          } catch (error) {
            // Skip this file and continue with other files
            console.error(error);
          }
        }),
      );

      const res = await injector.get(DocumentsProvider).insertDocuments({ document: newDocuments });
      return res.map(document => ({ document: document as IGetAllDocumentsResult }));
    },
    updateDocument: async (_, { fields, documentId }, { injector }) => {
      let postUpdateActions = async (): Promise<void> => void 0;

      try {
        let chargeId: string | undefined = undefined;

        if (fields.chargeId === EMPTY_UUID) {
          // case unlinked from charge
          const document = await injector
            .get(DocumentsProvider)
            .getDocumentsByIdLoader.load(documentId);
          if (!document) {
            throw new GraphQLError(`Document ID="${documentId}" not valid`);
          }
          if (document.charge_id) {
            const charge = await injector
              .get(ChargesProvider)
              .getChargeByIdLoader.load(document.charge_id);
            if (!charge) {
              throw new GraphQLError(
                `Former document's charge ID ("${fields.chargeId}") not valid`,
              );
            }

            // generate new charge
            const newCharge = await injector.get(ChargesProvider).generateCharge({
              ownerId: charge.owner_id,
              userDescription: 'Document unlinked from charge',
            });
            if (!newCharge || newCharge.length === 0) {
              throw new GraphQLError(
                `Failed to generate new charge for document ID="${documentId}"`,
              );
            }
            chargeId = newCharge?.[0]?.id;

            if (
              Number(charge.documents_count ?? 1) === 1 &&
              Number(charge.transactions_count ?? 0) === 0
            ) {
              postUpdateActions = async () => {
                try {
                  await deleteCharges([charge.id], injector);
                } catch (e) {
                  console.error(e);
                  throw new GraphQLError(
                    `Failed to delete the empty former charge ID="${charge.id}"`,
                  );
                }
                return postUpdateActions();
              };
            }
          }
        } else if (fields.chargeId) {
          // case new charge ID
          const charge = await injector
            .get(ChargesProvider)
            .getChargeByIdLoader.load(fields.chargeId);
          if (!charge) {
            throw new GraphQLError(`Charge ID="${fields.chargeId}" not valid`);
          }
          chargeId = charge.id;
        }

        const adjustedFields: IUpdateDocumentParams = {
          documentId,
          chargeId: chargeId ?? null,
          currencyCode: fields.amount?.currency ?? null,
          creditorId: fields.creditorId ?? null,
          debtorId: fields.debtorId ?? null,
          date: fields.date ? new Date(fields.date) : null,
          fileUrl: fields.file ? fields.file.toString() : null,
          imageUrl: fields.image ? fields.image.toString() : null,
          serialNumber: fields.serialNumber ?? null,
          totalAmount: fields.amount?.raw ?? null,
          type: fields.documentType ?? null,
          vatAmount: fields.vat?.raw ?? null,
          vatReportDateOverride: fields.vatReportDateOverride
            ? new Date(fields.vatReportDateOverride)
            : null,
          noVatAmount: fields.noVatAmount ?? null,
          isReviewed: true,
        };
        const res = await injector.get(DocumentsProvider).updateDocument({ ...adjustedFields });
        if (!res || res.length === 0) {
          throw new Error(`Document ID="${documentId}" not found`);
        }

        await postUpdateActions();

        return {
          document: res[0],
        };
      } catch (e) {
        return {
          __typename: 'CommonError',
          message: (e as Error)?.message ?? 'Unknown error',
        };
      }
    },
    deleteDocument: async (_, { documentId }, { injector }) => {
      const document = await injector
        .get(DocumentsProvider)
        .getDocumentsByIdLoader.load(documentId);
      if (!document) {
        throw new GraphQLError(`Document ID="${documentId}" not found`);
      }
      const res = await injector.get(DocumentsProvider).deleteDocument({ documentId });
      if (res.length === 1) {
        if (document.charge_id) {
          const charge = await injector
            .get(ChargesProvider)
            .getChargeByIdLoader.load(document.charge_id);
          if (charge && !charge.documents_count && !charge.transactions_count) {
            await deleteCharges([charge.id], injector);
          }
        }
        return true;
      }
      throw new GraphQLError(
        res.length === 0
          ? 'Document not found'
          : `More than one document found and deleted: ${res}`,
      );
    },
    insertDocument: async (_, { record }, { injector }) => {
      try {
        if (record.chargeId) {
          const charge = await injector
            .get(ChargesProvider)
            .getChargeByIdLoader.load(record.chargeId);

          if (!charge) {
            throw new Error(`Charge ID='${record.chargeId}' not found`);
          }
        }

        const newDocument: IInsertDocumentsParams['document']['0'] = {
          image: record.image ? record.image.toString() : null,
          file: record.file ? record.file.toString() : null,
          documentType: record.documentType ?? DocumentType.Unprocessed,
          serialNumber: record.serialNumber ?? null,
          date: record.date ? new Date(record.date) : null,
          amount: record.amount?.raw ?? null,
          currencyCode: record.amount?.currency ?? null,
          vat: record.vat?.raw ?? null,
          chargeId: record.chargeId ?? null,
          vatReportDateOverride: record.vatReportDateOverride
            ? new Date(record.vatReportDateOverride)
            : null,
          noVatAmount: record.noVatAmount ?? null,
          creditorId: record.creditorId ?? null,
          debtorId: record.debtorId ?? null,
        };
        const res = await injector
          .get(DocumentsProvider)
          .insertDocuments({ document: [{ ...newDocument }] });

        if (!res || res.length === 0) {
          throw new Error(`Failed to insert ledger record to charge ID='${record.chargeId}'`);
        }

        if (record.chargeId) {
          /* clear cache */
          injector.get(DocumentsProvider).getDocumentsByChargeIdLoader.clear(record.chargeId);
        }

        return { document: res[0] };
      } catch (e) {
        return {
          __typename: 'CommonError',
          message: `Error inserting new ledger record:\n  ${
            (e as Error)?.message ?? 'Unknown error'
          }`,
        };
      }
    },
    fetchIncomeDocuments: async (
      _,
      { ownerId },
      { injector, adminContext: { defaultAdminBusinessId } },
    ) => {
      const data = await injector.get(GreenInvoiceProvider).searchDocuments({
        input: { pageSize: 100, sort: 'creationDate' },
      });
      if (!data?.items) {
        throw new GraphQLError('Failed to fetch documents');
      }
      if (data.items.length === 0) {
        return [];
      }

      const documents = await injector.get(DocumentsProvider).getAllDocuments();
      const newDocuments = data.items.filter(
        item =>
          item &&
          !documents.some(
            doc =>
              doc.vat_amount === item.vat &&
              doc.total_amount === item.amount &&
              doc.serial_number === item.number &&
              optionalDateToTimelessDateString(doc.date) === item.documentDate,
          ) &&
          item.type !== '_300',
      );
      const addedDocs: IInsertDocumentsResult[] = [];

      await Promise.all(
        newDocuments.map(async greenInvoiceDoc => {
          if (!greenInvoiceDoc) {
            return;
          }

          const documentType = normalizeDocumentType(greenInvoiceDoc.type);
          const isOwnerCreditor =
            greenInvoiceDoc.amount > 0 && documentType !== DocumentType.CreditInvoice;

          try {
            // generate preview image via cloudinary
            const imagePromise = injector
              .get(CloudinaryProvider)
              .uploadInvoiceToCloudinary(greenInvoiceDoc.url.origin);

            // Generate parent charge
            const chargePromise = injector.get(ChargesProvider).generateCharge({
              ownerId,
              userDescription:
                greenInvoiceDoc.description && greenInvoiceDoc.description !== ''
                  ? greenInvoiceDoc.description
                  : 'Green Invoice generated charge',
            });

            // Get matching business
            const businessPromise = injector
              .get(BusinessesGreenInvoiceMatcherProvider)
              .getBusinessMatchByGreenInvoiceIdLoader.load(greenInvoiceDoc.client.id);

            const [{ imageUrl }, [charge], business] = await Promise.all([
              imagePromise,
              chargePromise,
              businessPromise,
            ]);

            if (!charge) {
              throw new Error('Failed to generate charge');
            }

            const counterpartyId = business?.business_id ?? null;

            // insert document
            const rawDocument: IInsertDocumentsParams['document']['0'] = {
              image: imageUrl,
              file: greenInvoiceDoc.url.origin,
              documentType,
              serialNumber: greenInvoiceDoc.number,
              date: greenInvoiceDoc.documentDate,
              amount: greenInvoiceDoc.amount,
              currencyCode: formatCurrency(greenInvoiceDoc.currency),
              vat: greenInvoiceDoc.vat,
              chargeId: charge.id,
              vatReportDateOverride: null,
              noVatAmount: null,
              creditorId: isOwnerCreditor ? defaultAdminBusinessId : counterpartyId,
              debtorId: isOwnerCreditor ? counterpartyId : defaultAdminBusinessId,
            };

            const newDocumentPromise = injector
              .get(DocumentsProvider)
              .insertDocuments({ document: [rawDocument] });

            const chargeDescriptionUpdate = new Promise(resolve => {
              const income = greenInvoiceDoc.income;
              if (
                !income ||
                income.length === 0 ||
                !income[0]?.description ||
                income[0].description === ''
              ) {
                resolve(undefined);
              }

              const userDescription = income
                .filter(item => item?.description)
                .map(item => item!.description)
                .join(', ');

              injector
                .get(ChargesProvider)
                .updateCharge({
                  chargeId: charge.id,
                  userDescription,
                })
                .then(() => resolve(undefined));
            });

            const [newDocument] = await Promise.all([newDocumentPromise, chargeDescriptionUpdate]);

            addedDocs.push(newDocument[0]);
          } catch (e) {
            throw new GraphQLError(
              `Error adding Green Invoice document: ${e}\n\n${JSON.stringify(
                greenInvoiceDoc,
                null,
                2,
              )}`,
            );
          }
        }),
      );

      return addedDocs;
    },
    generateMonthlyClientDocuments: async (_, __, { injector }) => {
      const jsonArray: Array<{
        businessId: string;
        amount: number;
        currency: GreenInvoiceCurrency;
      }> = [
        {
          businessId: '147d3415-55e3-497f-acba-352dcc37cb8d', // uri test
          amount: 1000,
          currency: Currency.Ils,
        },
      ];

      const errors: string[] = [];

      const proformaProtos = await Promise.all(
        jsonArray.map(async json => {
          const businessPromise = injector
            .get(BusinessesProvider)
            .getBusinessByIdLoader.load(json.businessId);
          const businessGreenInvoiceMatchPromise = injector
            .get(BusinessesGreenInvoiceMatcherProvider)
            .getBusinessMatchByIdLoader.load(json.businessId);
          const [business, businessGreenInvoiceMatch] = await Promise.all([
            businessPromise,
            businessGreenInvoiceMatchPromise,
          ]);

          if (!business) {
            throw new GraphQLError(`Business ID="${json.businessId}" not found`);
          }

          if (!businessGreenInvoiceMatch) {
            throw new GraphQLError(
              `Green invoice match not found for business ID="${json.businessId}"`,
            );
          }

          const today = new Date();
          const monthStart = dateToTimelessDateString(startOfMonth(today));
          const monthEnd = dateToTimelessDateString(endOfMonth(today));
          const year = today.getFullYear() + (today.getMonth() === 0 ? -1 : 0);
          const month = format(subMonths(today, 1), 'MMMM');

          const documentInput: DocumentInput_Input & { businessName: string } = {
            businessName: business.name,
            type: getGreenInvoiceDocumentType(
              businessGreenInvoiceMatch.document_type as DocumentType,
            ),
            remarks: businessGreenInvoiceMatch.remark ?? undefined,
            date: monthStart,
            dueDate: monthEnd,
            lang: 'en',
            currency: json.currency,
            vatType: 1,
            rounding: false,
            signed: true,
            attachment: true,
            client: {
              id: businessGreenInvoiceMatch.green_invoice_id,
              emails: [...(businessGreenInvoiceMatch.emails ?? []), 'ap@the-guild.dev'],
            },
            income: [
              {
                description: `GraphQL Hive Enterprise License - ${month} ${year}`,
                quantity: 1,
                price: json.amount,
                currency: json.currency,
                vatType: 1,
              },
            ],
          };

          return documentInput;
        }),
      );

      for (const proformaProto of proformaProtos) {
        const { businessName, ...input } = proformaProto;
        await injector
          .get(GreenInvoiceProvider)
          .addDocuments({ input })
          .then(res => {
            if (res && 'errorMessage' in res) {
              errors.push(`${businessName}: ${res.errorMessage}`);
            }
          })
          .catch(e => {
            console.error(e);
            errors.push(`${businessName}: ${e.message}`);
          });
      }

      return {
        success: true,
        errors,
      };
    },
  },
  Document: {
    __resolveType: (documentRoot, _context, _info) => {
      switch (documentRoot?.type) {
        case DocumentType.Invoice: {
          return 'Invoice';
        }
        case DocumentType.Receipt: {
          return 'Receipt';
        }
        case DocumentType.InvoiceReceipt: {
          return 'InvoiceReceipt';
        }
        case DocumentType.CreditInvoice: {
          return 'CreditInvoice';
        }

        case DocumentType.Proforma: {
          return 'Proforma';
        }
        default: {
          return 'Unprocessed';
        }
      }
    },
  },
  UpdateDocumentResult: {
    __resolveType: (obj, _context, _info) => {
      if ('__typename' in obj && obj.__typename === 'CommonError') return 'CommonError';
      return 'UpdateDocumentSuccessfulResult';
    },
  },
  InsertDocumentResult: {
    __resolveType: (obj, _context, _info) => {
      if ('__typename' in obj && obj.__typename === 'CommonError') return 'CommonError';
      return 'InsertDocumentSuccessfulResult';
    },
  },
  UploadDocumentResult: {
    __resolveType: (obj, _context, _info) => {
      if ('__typename' in obj && obj.__typename === 'CommonError') return 'CommonError';
      return 'UploadDocumentSuccessfulResult';
    },
  },
  Invoice: {
    ...commonDocumentsFields,
    ...commonFinancialDocumentsFields,
  },
  InvoiceReceipt: {
    ...commonDocumentsFields,
    ...commonFinancialDocumentsFields,
  },
  CreditInvoice: {
    ...commonDocumentsFields,
    ...commonFinancialDocumentsFields,
  },
  Proforma: {
    ...commonDocumentsFields,
    ...commonFinancialDocumentsFields,
  },
  Unprocessed: {
    ...commonDocumentsFields,
  },
  OtherDocument: {
    ...commonDocumentsFields,
  },
  Receipt: {
    ...commonDocumentsFields,
    ...commonFinancialDocumentsFields,
  },
  CommonCharge: commonChargeFields,
  FinancialCharge: commonChargeFields,
  ConversionCharge: commonChargeFields,
  SalaryCharge: commonChargeFields,
  InternalTransferCharge: commonChargeFields,
  DividendCharge: commonChargeFields,
  BusinessTripCharge: commonChargeFields,
  MonthlyVatCharge: commonChargeFields,
  BankDepositCharge: commonChargeFields,
  CreditcardBankCharge: commonChargeFields,
};
