import { endOfDay, startOfDay } from 'date-fns';
import { GraphQLError } from 'graphql';
import { DocumentType } from '../../../shared/enums.js';
import { dateToTimelessDateString, hashStringToInt } from '../../../shared/helpers/index.js';
import { AdminContextProvider } from '../../admin-context/providers/admin-context.provider.js';
import { DeelClientProvider } from '../../app-providers/deel/deel-client.provider.js';
import { uploadToCloudinary } from '../../documents/helpers/upload.helper.js';
import { DocumentsProvider } from '../../documents/providers/documents.provider.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';
const DEEL_BUSINESS_ID = '8d34f668-7233-4ce3-9c9c-82550b0839ff'; // TODO: replace with DB based business id
export function isDeelDocument(document) {
    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, document, ledgerEntry, ledgerEntries, updateLedgerBalance) {
    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;
        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;
}
export const createDeelInvoiceMatchFromUnmatchedInvoice = (invoice) => ({
    ...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, match, injector, ownerId) {
    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 = {
            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,
            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) {
    return raw === '' ? null : raw;
}
function nullifyFeeInvoices(contractType, contractId) {
    const feeTypes = ['payment_processing_fee', 'eor_management_fee', 'unknown'];
    if (feeTypes.includes(contractType)) {
        return null;
    }
    return contractId;
}
export function convertMatchToDeelInvoiceRecord(match, documentId) {
    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,
        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,
        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, workers) {
    const contractIds = workers?.map(w => w.contract_id).filter(id => !!id) ?? [];
    const contracts = await injector
        .get(DeelContractsProvider)
        .getEmployeeByContractIdLoader.loadMany(contractIds)
        .then(contract => contract.filter(id => !!id && !(id instanceof Error)));
    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) {
    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))));
        const existingContractIds = new Set(contracts.filter(c => !!c && !(c instanceof Error)).map(c => c.contract_id));
        const filteredInvoices = [];
        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) {
    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, receipts) {
    const receiptsBreakDown = [];
    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;
}
export function getContractsFromPaymentBreakdowns(matches) {
    const contractsMap = new Map();
    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, 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, receipts) {
    const receiptChargeMap = await injector.get(DeelInvoicesProvider).getReceiptToCharge();
    const invoiceChargeMap = new Map();
    const newReceipts = [];
    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, invoices, receipts) {
    const matches = [];
    const unmatched = [];
    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 = {};
            Object.entries(optionalMatches[0]).map(([key, value]) => {
                adjustedBreakdown[`breakdown_${key}`] = value;
            });
            matches.push({ ...adjustedBreakdown, ...invoice });
        }
        else {
            throw new Error(`No payment match found for invoice ${invoice.id}`);
        }
    });
    return { matches, unmatched };
}
export async function insertDeelInvoiceRecord(injector, match, chargeId) {
    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 });
    }
}
//# sourceMappingURL=deel.helper.js.map