import { endOfDay, startOfDay } from 'date-fns';
import { GraphQLError } from 'graphql';
import type { Injector } from 'graphql-modules';
import { Currency, DocumentType } from '../../../shared/enums.js';
import { dateToTimelessDateString, hashStringToInt } from '../../../shared/helpers/index.js';
import type { LedgerProto, StrictLedgerProto } from '../../../shared/types/index.js';
import { AdminContextProvider } from '../../admin-context/providers/admin-context.provider.js';
import { DeelClientProvider } from '../../app-providers/deel/deel-client.provider.js';
import type {
  DeelWorker,
  Invoice,
  PaymentBreakdownRecord,
  PaymentReceipts,
} from '../../app-providers/deel/schemas.js';
import { uploadToCloudinary } from '../../documents/helpers/upload.helper.js';
import { DocumentsProvider } from '../../documents/providers/documents.provider.js';
import type {
  IGetDocumentsByChargeIdResult,
  IInsertDocumentsParams,
} from '../../documents/types.js';
import { TaxCategoriesProvider } from '../../financial-entities/providers/tax-categories.provider.js';
import { DeelContractsProvider } from '../providers/deel-contracts.provider.js';
import { DeelInvoicesProvider } from '../providers/deel-invoices.provider.js';
import type {
  IGetEmployeeIDsByContractIdsResult,
  IInsertDeelInvoiceRecordsParams,
} from '../types.js';

const DEEL_BUSINESS_ID = '8d34f668-7233-4ce3-9c9c-82550b0839ff'; // TODO: replace with DB based business id

export function isDeelDocument(document: IGetDocumentsByChargeIdResult): boolean {
  const isDeelSide =
    document.creditor_id === DEEL_BUSINESS_ID || document.debtor_id === DEEL_BUSINESS_ID;
  const isFinancialDocument =
    document.type === 'INVOICE' ||
    document.type === 'INVOICE_RECEIPT' ||
    document.type === 'CREDIT_INVOICE';

  return isDeelSide && isFinancialDocument;
}

export async function getDeelEmployeeId(
  injector: Injector,
  document: IGetDocumentsByChargeIdResult,
  ledgerEntry: StrictLedgerProto,
  ledgerEntries: LedgerProto[],
  updateLedgerBalance: (entry: LedgerProto) => void,
): Promise<void> {
  if (!isDeelDocument(document)) {
    return;
  }

  const isDeelCreditor = ledgerEntry.creditAccountID1 === DEEL_BUSINESS_ID;
  // naive fetch employee id from deel
  let employeeId = await injector
    .get(DeelContractsProvider)
    .getEmployeeIdByDocumentIdLoader.load(document.id);

  if (!employeeId && document.date && document.type) {
    // figure out through deel records
    const records = await injector
      .get(DeelInvoicesProvider)
      .getInvoicesByIssueDates(startOfDay(document.date), endOfDay(document.date));

    const matchingRecord = records.find(r => {
      if (dateToTimelessDateString(r.issued_at) !== dateToTimelessDateString(document.date!)) {
        return false;
      }
      if (r.label !== document.serial_number) {
        return false;
      }
      if (r.currency !== document.currency_code) {
        return false;
      }
      if (Number(r.total) !== document.total_amount) {
        if (records.length === 1) {
          return false;
        }
        const spreadRecords = records.filter(r => r.label === document.serial_number);
        if (spreadRecords.length === 1) {
          return false;
        }
        const totalAmount = spreadRecords.reduce((acc, r) => acc + Number(r.amount), 0);
        if (totalAmount !== document.total_amount) {
          return false;
        }
      }
      return true;
    });
    if (matchingRecord?.contract_id) {
      employeeId = await injector
        .get(DeelContractsProvider)
        .getEmployeeIDByContractIdLoader.load(matchingRecord.contract_id);
    }
  }

  if (employeeId) {
    // get employee tax category
    const taxCategoryId = await injector
      .get(TaxCategoriesProvider)
      .taxCategoryByBusinessIDsLoader.load(employeeId)
      .then(taxCategory => taxCategory?.id);
    let newEntry: LedgerProto;
    if (isDeelCreditor) {
      newEntry = {
        ...ledgerEntry,
        debitAccountID1: employeeId,
      };
      ledgerEntry.creditAccountID1 = employeeId;
      ledgerEntry.debitAccountID1 = taxCategoryId ?? ledgerEntry.debitAccountID1;
    } else {
      newEntry = {
        ...ledgerEntry,
        creditAccountID1: employeeId,
      };
      ledgerEntry.creditAccountID1 = taxCategoryId ?? ledgerEntry.creditAccountID1;
      ledgerEntry.debitAccountID1 = employeeId;
    }
    updateLedgerBalance(newEntry);
    ledgerEntries.push(newEntry);
  }

  return;
}

type Prefixer<Type> = {
  [Property in keyof Type as `breakdown_${string & Property}`]: Type[Property];
};
export type PrefixedBreakdown = Prefixer<PaymentBreakdownRecord & { receipt_id: string }>;
export type DeelInvoiceMatch = Invoice & PrefixedBreakdown;

export const createDeelInvoiceMatchFromUnmatchedInvoice = (invoice: Invoice): DeelInvoiceMatch => ({
  ...invoice,
  breakdown_receipt_id: '',
  breakdown_adjustment: '0.00',
  breakdown_approve_date: '',
  breakdown_approvers: '',
  breakdown_bonus: '0.00',
  breakdown_commissions: '0.00',
  breakdown_contract_country: '',
  breakdown_contract_start_date: '',
  breakdown_contract_type: '',
  breakdown_contractor_email: '',
  breakdown_contractor_employee_name: '',
  breakdown_contractor_unique_identifier: '',
  breakdown_currency: '',
  breakdown_date: '',
  breakdown_deductions: '0.00',
  breakdown_expenses: '0.00',
  breakdown_frequency: '',
  breakdown_general_ledger_account: '',
  breakdown_group_id: '',
  breakdown_invoice_id: '',
  breakdown_others: '0.00',
  breakdown_overtime: '0.00',
  breakdown_payment_currency: invoice.currency,
  breakdown_payment_date: '',
  breakdown_pro_rata: '0.00',
  breakdown_processing_fee: '0.00',
  breakdown_work: '0.00',
  breakdown_total: '',
  breakdown_total_payment_currency: invoice.amount,
});

export async function uploadDeelInvoice(
  chargeId: string,
  match: DeelInvoiceMatch,
  injector: Injector,
  ownerId: string,
): Promise<string> {
  try {
    // fetch file from Deel
    const file = await injector.get(DeelClientProvider).getSalaryInvoiceFile(match.id);

    const fileHashPromise = file.text().then(content => hashStringToInt(content).toString());

    // upload file to cloudinary
    const fileUrlsPromise = uploadToCloudinary(injector, file);

    const [{ fileUrl, imageUrl }, fileHash] = await Promise.all([fileUrlsPromise, fileHashPromise]);

    // create the new document object
    const newDocumentFromInvoice: IInsertDocumentsParams['documents'][number] = {
      ownerId,
      image: imageUrl ?? null,
      file: fileUrl ?? null,
      documentType:
        match.breakdown_contract_type === 'prepaid_billing'
          ? DocumentType.Other
          : match.status === 'refunded'
            ? DocumentType.CreditInvoice
            : DocumentType.Invoice,
      serialNumber: match.label,
      date: match.issued_at,
      amount: Number(match.breakdown_total_payment_currency),
      currencyCode: match.breakdown_payment_currency as Currency,
      vat: Number(match.vat_total),
      chargeId,
      vatReportDateOverride: null,
      noVatAmount: null,
      debtorId: ownerId,
      creditorId: DEEL_BUSINESS_ID,
      allocationNumber: null,
      exchangeRateOverride: null,
      fileHash,
      description: match.breakdown_contractor_employee_name,
      remarks: match.id,
    };

    // upload the document
    const [document] = await injector.get(DocumentsProvider).insertDocuments({
      documents: [newDocumentFromInvoice],
    });

    if (!document.id) {
      throw new Error('Document not uploaded to DB');
    }

    return document.id;
  } catch (error) {
    const message = 'Error uploading Deel invoice';
    console.error(`${message}: ${error}`);
    throw new Error(message, { cause: error });
  }
}

function nullifyEmptyStrings(raw: string) {
  return raw === '' ? null : raw;
}

function nullifyFeeInvoices(contractType: string, contractId: string | null): string | null {
  const feeTypes: readonly string[] = ['payment_processing_fee', 'eor_management_fee', 'unknown'];
  if (feeTypes.includes(contractType)) {
    return null;
  }
  return contractId;
}

export function convertMatchToDeelInvoiceRecord(
  match: DeelInvoiceMatch,
  documentId: string,
): IInsertDeelInvoiceRecordsParams {
  return {
    adjustment: match.breakdown_adjustment,
    amount: match.amount,
    approveDate: nullifyEmptyStrings(match.breakdown_approve_date),
    approvers: match.breakdown_approvers,
    bonus: match.breakdown_bonus,
    commissions: match.breakdown_commissions,
    contractCountry: nullifyEmptyStrings(match.breakdown_contract_country),
    contractId: nullifyFeeInvoices(match.breakdown_contract_type, match.contract_id),
    contractStartDate: nullifyEmptyStrings(match.breakdown_contract_start_date),
    contractType: nullifyEmptyStrings(match.breakdown_contract_type),
    contractorEmail: nullifyEmptyStrings(match.breakdown_contractor_email),
    contractorEmployeeName: match.breakdown_contractor_employee_name,
    contractorUniqueIdentifier: nullifyEmptyStrings(match.breakdown_contractor_unique_identifier),
    createdAt: match.created_at,
    currency: match.currency as Currency,
    deductions: match.breakdown_deductions,
    deelFee: match.deel_fee,
    documentId,
    dueDate: match.due_date,
    expenses: match.breakdown_expenses,
    frequency: nullifyEmptyStrings(match.breakdown_frequency),
    generalLedgerAccount: nullifyEmptyStrings(match.breakdown_general_ledger_account),
    groupId: nullifyEmptyStrings(match.breakdown_group_id),
    id: match.id,
    isOverdue: match.is_overdue,
    issuedAt: match.issued_at,
    label: match.label,
    others: match.breakdown_others,
    overtime: match.breakdown_overtime,
    paidAt: match.paid_at,
    paymentCurrency: match.breakdown_payment_currency as Currency,
    paymentId: match.breakdown_receipt_id,
    processingFee: match.breakdown_processing_fee,
    proRata: match.breakdown_pro_rata,
    status: match.status,
    total: match.total,
    totalPaymentCurrency: match.breakdown_total_payment_currency,
    vatId: nullifyEmptyStrings(match.vat_id),
    vatPercentage: nullifyEmptyStrings(match.vat_percentage),
    vatTotal: match.vat_total,
    work: match.breakdown_work,
    recipientLegalEntityId: match.recipient_legal_entity_id,
  };
}

export async function getDeelChargeDescription(
  injector: Injector,
  workers?: DeelWorker[],
): Promise<string> {
  const contractIds = (workers?.map(w => w.contract_id).filter(id => !!id) as string[]) ?? [];
  const contracts = await injector
    .get(DeelContractsProvider)
    .getEmployeeByContractIdLoader.loadMany(contractIds)
    .then(
      contract =>
        contract.filter(
          id => !!id && !(id instanceof Error),
        ) as IGetEmployeeIDsByContractIdsResult[],
    );
  const workerNames = contracts.map(c => c.contractor_name.split(' ')[0]);
  const workersDescription = workerNames?.length ? ` for ${workerNames.join(', ')}` : '';
  const description = `Deel payment${workersDescription}`;
  return description;
}

export async function fetchAndFilterInvoices(injector: Injector) {
  try {
    const invoices = await injector.get(DeelClientProvider).getSalaryInvoices(); // TODO: enable setting up period
    const contracts = await injector
      .get(DeelContractsProvider)
      .getEmployeeByContractIdLoader.loadMany(
        Array.from(new Set(invoices.map(i => i.contract_id).filter(id => !!id) as string[])),
      );
    const existingContractIds = new Set(
      (
        contracts.filter(c => !!c && !(c instanceof Error)) as IGetEmployeeIDsByContractIdsResult[]
      ).map(c => c.contract_id),
    );

    const filteredInvoices: Invoice[] = [];

    for (const invoice of invoices) {
      if (invoice.contract_id && !existingContractIds.has(invoice.contract_id)) {
        const deelContract = await injector
          .get(DeelClientProvider)
          .getContractDetails(invoice.contract_id);
        console.debug(`New contract found: ${invoice.contract_id}
          ${JSON.stringify(deelContract, null, 2)}`);
        throw new Error(`Deel contract ID [${invoice.contract_id}] not found in DB. Match business to the following contract details:
          id: ${deelContract.data.id}
          contractor_id: ${deelContract.data.worker?.id}
          contractor_name: ${deelContract.data.worker?.full_name}
          contract_start_date: ${deelContract.data.start_date}`);
      }
      const dbInvoice = await injector
        .get(DeelInvoicesProvider)
        .getInvoicesByIdLoader.load(invoice.id)
        .catch(e => {
          const message = 'Error fetching invoice by ID';
          console.error(`${message}: ${e}`);
          throw new Error(message);
        });
      if (!dbInvoice) {
        filteredInvoices.push(invoice);
      }
    }

    return filteredInvoices;
  } catch (error) {
    const message = 'Error fetching Deel invoices';
    console.error(`${message}: ${error}`);
    throw new Error(message, { cause: error });
  }
}

export async function fetchReceipts(injector: Injector) {
  try {
    const res = await injector.get(DeelClientProvider).getPaymentReceipts(); // TODO: use PERIOD_IN_MONTHS
    return res.data.rows;
  } catch (error) {
    const message = 'Error fetching Deel receipts';
    console.error(`${message}: ${error}`);
    throw new Error(message, { cause: error });
  }
}

export async function fetchPaymentBreakdowns(injector: Injector, receipts: PaymentReceipts[]) {
  const receiptsBreakDown: Array<PaymentBreakdownRecord & { receipt_id: string }> = [];

  for (const receipt of receipts) {
    if (receipt.id) {
      const breakDown = await injector.get(DeelClientProvider).getPaymentBreakdown(receipt.id);
      receiptsBreakDown.push(...breakDown.data.map(row => ({ ...row, receipt_id: receipt.id })));
    }
  }

  return receiptsBreakDown;
}

type ContractInfo = {
  contract_country: string;
  contract_start_date: string;
  contract_type: string;
  contractor_email: string;
  contractor_employee_name: string;
  contractor_unique_identifier: string;
};

export function getContractsFromPaymentBreakdowns(matches: DeelInvoiceMatch[]) {
  const contractsMap = new Map<string, ContractInfo>();
  for (const match of matches) {
    if (!match.contract_id) {
      continue;
    }
    const existing = contractsMap.get(match.contract_id);
    if (existing) {
      if (match.breakdown_contractor_unique_identifier === '') {
        // TODO: this is a temporary fix to handle fee invoices with alternative contractor info
        continue;
      }
      // verify consistency
      if (
        existing.contract_country !== match.breakdown_contract_country ||
        existing.contract_start_date !== match.breakdown_contract_start_date ||
        existing.contract_type !== match.breakdown_contract_type ||
        existing.contractor_email !== match.breakdown_contractor_email ||
        existing.contractor_employee_name !== match.breakdown_contractor_employee_name ||
        existing.contractor_unique_identifier !== match.breakdown_contractor_unique_identifier
      ) {
        throw new Error(`Inconsistent contract info for contract_id [${match.contract_id}]`);
      }
    } else {
      contractsMap.set(match.contract_id, {
        contract_country: match.breakdown_contract_country,
        contract_start_date: match.breakdown_contract_start_date,
        contract_type: match.breakdown_contract_type,
        contractor_email: match.breakdown_contractor_email,
        contractor_employee_name: match.breakdown_contractor_employee_name,
        contractor_unique_identifier: match.breakdown_contractor_unique_identifier,
      });
    }
  }
  return contractsMap;
}

export async function validateContracts(
  contractsInfo: Map<string, ContractInfo>,
  injector: Injector,
) {
  await Promise.all(
    Array.from(contractsInfo.keys()).map(async contractId => {
      try {
        const deelEmployee = await injector
          .get(DeelContractsProvider)
          .getEmployeeByContractIdLoader.load(contractId);
        if (!deelEmployee) {
          throw new Error(`Deel contract ID [${contractId}] not found in DB`);
        }
      } catch {
        try {
          const contractInfo = contractsInfo.get(contractId)!;
          await injector.get(DeelContractsProvider).insertDeelContract({
            contractId,
            contractorId: contractInfo.contractor_unique_identifier,
            contractorName: contractInfo.contractor_employee_name,
            contractStartDate: contractInfo.contract_start_date,
            businessId: DEEL_BUSINESS_ID, // TODO: replace with DB based business id
          });
        } catch (error) {
          const message = `Error adding Deel contract [${contractId}] during validation`;
          console.error(message, error);
          throw new Error(message, { cause: error });
        }
      }
    }),
  );
}

export async function getChargeMatchesForPayments(injector: Injector, receipts: PaymentReceipts[]) {
  const receiptChargeMap = await injector.get(DeelInvoicesProvider).getReceiptToCharge();
  const invoiceChargeMap = new Map<string, string>();
  const newReceipts: PaymentReceipts[] = [];

  receipts.map(receipt => {
    const chargeId = receiptChargeMap.get(receipt.id);
    if (chargeId) {
      receipt.invoices?.map(invoice => invoiceChargeMap.set(invoice.id, chargeId));
    } else {
      newReceipts.push(receipt);
    }
  });

  return { receiptChargeMap, invoiceChargeMap, newReceipts };
}

export async function matchInvoicesWithPayments(
  injector: Injector,
  invoices: Invoice[],
  receipts: PaymentReceipts[],
) {
  const matches: DeelInvoiceMatch[] = [];
  const unmatched: Invoice[] = [];

  const paymentBreakdowns = await fetchPaymentBreakdowns(injector, receipts);

  invoices.map(invoice => {
    if (
      invoice.status === 'processed' ||
      invoice.total === '0.00' ||
      invoice.status === 'refunded'
    ) {
      unmatched.push(invoice);
      return;
    }

    const optionalMatches = paymentBreakdowns.filter(receipt => invoice.id === receipt.invoice_id);
    if (optionalMatches.length === 1) {
      const adjustedBreakdown: Record<string, unknown> = {};
      Object.entries(optionalMatches[0]).map(([key, value]) => {
        adjustedBreakdown[`breakdown_${key}`] = value;
      });
      matches.push({ ...(adjustedBreakdown as PrefixedBreakdown), ...invoice });
    } else {
      throw new Error(`No payment match found for invoice ${invoice.id}`);
    }
  });

  return { matches, unmatched };
}

export async function insertDeelInvoiceRecord(
  injector: Injector,
  match: DeelInvoiceMatch,
  chargeId: string,
) {
  try {
    const { ownerId } = await injector.get(AdminContextProvider).getVerifiedAdminContext();
    const documentId = await uploadDeelInvoice(chargeId, match, injector, ownerId);

    await injector
      .get(DeelInvoicesProvider)
      .insertDeelInvoiceRecords(convertMatchToDeelInvoiceRecord(match, documentId));
  } catch (error) {
    const message = 'Error uploading Deel invoice record';
    console.error(`${message}: ${error}`);
    if (error instanceof GraphQLError) {
      throw error;
    }
    throw new Error(message, { cause: error });
  }
}
