// External Libraries and Node.js Modules
import fs from 'fs-extra';
import * as path from 'path';
import c from 'chalk';
import open from 'open';
import * as split from 'split';
import { PromisePool } from '@supercharge/promise-pool';
// Salesforce Specific and Other Specific Libraries
import { SfError } from '@salesforce/core';
import Papa from 'papaparse';
import ExcelJS from 'exceljs';
// Project Specific Utilities
import { getCurrentGitBranch, isCI, isGitRepo, uxLog } from './index.js';
import { bulkQuery, soqlQuery, bulkQueryByChunks } from './apiUtils.js';
import { prompts } from './prompts.js';
import { getApiVersion, getReportDirectory } from '../../config/index.js';
import { WebSocketClient } from '../websocketClient.js';
import { FileDownloader } from './fileDownloader.js';
export const filesFolderRoot = path.join('.', 'scripts', 'files');
export class FilesExporter {
    filesPath;
    conn;
    pollTimeout;
    recordsChunkSize;
    startChunkNumber;
    parentRecordsChunkSize;
    commandThis;
    dtl = null; // export config
    exportedFilesFolder = '';
    recordsChunk = [];
    chunksNumber = 1;
    recordsChunkQueue = [];
    recordsChunkQueueRunning = false;
    queueInterval;
    bulkApiRecordsEnded = false;
    recordChunksNumber = 0;
    totalSoqlRequests = 0;
    totalParentRecords = 0;
    parentRecordsWithFiles = 0;
    recordsIgnored = 0;
    filesDownloaded = 0;
    filesErrors = 0;
    filesIgnoredType = 0;
    filesIgnoredExisting = 0;
    apiUsedBefore = 0;
    apiLimit = 0;
    constructor(filesPath, conn, options, commandThis) {
        this.filesPath = filesPath;
        this.conn = conn;
        this.pollTimeout = options?.pollTimeout || 600000;
        this.recordsChunkSize = options?.recordsChunkSize || 1000;
        this.parentRecordsChunkSize = 100000;
        this.startChunkNumber = options?.startChunkNumber || 0;
        this.commandThis = commandThis;
        if (options.exportConfig) {
            this.dtl = options.exportConfig;
        }
    }
    async processExport() {
        // Get config
        if (this.dtl === null) {
            this.dtl = await getFilesWorkspaceDetail(this.filesPath);
        }
        uxLog("action", this.commandThis, c.cyan(`Exporting files from ${c.green(this.dtl.full_label)} ...`));
        uxLog("log", this.commandThis, c.italic(c.grey(this.dtl.description)));
        // Make sure export folder for files is existing
        this.exportedFilesFolder = path.join(this.filesPath, 'export');
        await fs.ensureDir(this.exportedFilesFolder);
        await this.calculateApiConsumption();
        this.startQueue();
        await this.processParentRecords();
        await this.queueCompleted();
        return await this.buildResult();
    }
    // Calculate API consumption
    async calculateApiConsumption() {
        const countSoqlQuery = this.dtl.soqlQuery.replace(/SELECT (.*) FROM/gi, 'SELECT COUNT() FROM');
        this.totalSoqlRequests++;
        const countSoqlQueryRes = await soqlQuery(countSoqlQuery, this.conn);
        this.chunksNumber = Math.round(countSoqlQueryRes.totalSize / this.recordsChunkSize);
        const estimatedApiCalls = Math.round(this.chunksNumber * 2) + 1;
        this.apiUsedBefore = this.conn?.limitInfo?.apiUsage?.used
            ? this.conn.limitInfo.apiUsage.used - 1
            : this.apiUsedBefore;
        this.apiLimit = this.conn?.limitInfo?.apiUsage?.limit;
        // Check if there are enough API calls available
        if (this.apiLimit - this.apiUsedBefore < estimatedApiCalls + 1000) {
            throw new SfError(`You don't have enough API calls available (${c.bold(this.apiLimit - this.apiUsedBefore)}) to perform this export that could consume ${c.bold(estimatedApiCalls)} API calls`);
        }
        // Request user confirmation
        if (!isCI) {
            const warningMessage = c.cyanBright(`This export of files could run on ${c.bold(c.yellow(countSoqlQueryRes.totalSize))} records, in ${c.bold(c.yellow(this.chunksNumber))} chunks, and consume up to ${c.bold(c.yellow(estimatedApiCalls))} API calls on the ${c.bold(c.yellow(this.apiLimit - this.apiUsedBefore))} remaining API calls. Do you want to proceed ?`);
            const promptRes = await prompts({
                type: 'confirm',
                message: warningMessage,
                description: 'Proceed with the operation despite API usage warnings'
            });
            if (promptRes.value !== true) {
                throw new SfError('Command cancelled by user');
            }
            if (this.startChunkNumber === 0) {
                uxLog("warning", this, c.yellow(c.italic('Use --startchunknumber command line argument if you do not want to start from first chunk')));
            }
        }
    }
    // Run chunks one by one, and don't wait to have all the records fetched to start it
    startQueue() {
        this.queueInterval = setInterval(async () => {
            if (this.recordsChunkQueueRunning === false && this.recordsChunkQueue.length > 0) {
                this.recordsChunkQueueRunning = true;
                const recordChunk = this.recordsChunkQueue.shift();
                await this.processRecordsChunk(recordChunk);
                this.recordsChunkQueueRunning = false;
                // Manage last chunk
            }
            else if (this.bulkApiRecordsEnded === true &&
                this.recordsChunkQueue.length === 0 &&
                this.recordsChunk.length > 0) {
                const recordsToProcess = [...this.recordsChunk];
                this.recordsChunk = [];
                this.recordsChunkQueue.push(recordsToProcess);
            }
        }, 1000);
    }
    // Wait for the queue to be completed
    async queueCompleted() {
        await new Promise((resolve) => {
            const completeCheckInterval = setInterval(async () => {
                if (this.bulkApiRecordsEnded === true &&
                    this.recordsChunkQueueRunning === false &&
                    this.recordsChunkQueue.length === 0 &&
                    this.recordsChunk.length === 0) {
                    clearInterval(completeCheckInterval);
                    resolve(true);
                }
                if (globalThis.sfdxHardisFatalError === true) {
                    uxLog("error", this, c.red('Fatal error while processing chunks queue'));
                    process.exit(1);
                }
            }, 1000);
        });
        clearInterval(this.queueInterval);
        this.queueInterval = null;
    }
    async processParentRecords() {
        // Query parent records using SOQL defined in export.json file
        this.totalSoqlRequests++;
        this.conn.bulk.pollTimeout = this.pollTimeout || 600000; // Increase timeout in case we are on a bad internet connection or if the bulk api batch is queued
        // Use bulkQueryByChunks to handle large queries
        const queryRes = await bulkQueryByChunks(this.dtl.soqlQuery, this.conn, this.parentRecordsChunkSize);
        for (const record of queryRes.records) {
            this.totalParentRecords++;
            const parentRecordFolderForFiles = path.resolve(path.join(this.exportedFilesFolder, record[this.dtl.outputFolderNameField] || record.Id));
            if (this.dtl.overwriteParentRecords !== true && fs.existsSync(parentRecordFolderForFiles)) {
                uxLog("log", this, c.grey(`Skipped record - ${record[this.dtl.outputFolderNameField] || record.Id} - Record files already downloaded`));
                this.recordsIgnored++;
                continue;
            }
            await this.addToRecordsChunk(record);
        }
        this.bulkApiRecordsEnded = true;
    }
    async addToRecordsChunk(record) {
        this.recordsChunk.push(record);
        // If chunk size is reached , process the chunk of records
        if (this.recordsChunk.length === this.recordsChunkSize) {
            const recordsToProcess = [...this.recordsChunk];
            this.recordsChunk = [];
            this.recordsChunkQueue.push(recordsToProcess);
        }
    }
    async processRecordsChunk(records) {
        this.recordChunksNumber++;
        if (this.recordChunksNumber < this.startChunkNumber) {
            uxLog("action", this, c.cyan(`Skip parent records chunk #${this.recordChunksNumber} because it is lesser than ${this.startChunkNumber}`));
            return;
        }
        uxLog("action", this, c.cyan(`Processing parent records chunk #${this.recordChunksNumber} on ${this.chunksNumber} (${records.length} records) ...`));
        // Process records in batches of 200 for Attachments and 1000 for ContentVersions to avoid hitting the SOQL query limit
        const attachmentBatchSize = 200;
        const contentVersionBatchSize = 1000;
        for (let i = 0; i < records.length; i += attachmentBatchSize) {
            const batch = records.slice(i, i + attachmentBatchSize);
            // Request all Attachment related to all records of the batch using REST API
            const parentIdIn = batch.map((record) => `'${record.Id}'`).join(',');
            const attachmentQuery = `SELECT Id, Name, ContentType, ParentId FROM Attachment WHERE ParentId IN (${parentIdIn})`;
            this.totalSoqlRequests++;
            const attachments = await this.conn.query(attachmentQuery);
            if (attachments.records.length > 0) {
                // Download attachments using REST API
                await PromisePool.withConcurrency(5)
                    .for(attachments.records)
                    .process(async (attachment) => {
                    try {
                        await this.downloadAttachmentFile(attachment, batch);
                    }
                    catch (e) {
                        this.filesErrors++;
                        uxLog("error", this, c.red('Download file error: ' + attachment.Name + '\n' + e));
                    }
                });
            }
            else {
                uxLog("log", this, c.grey('No Attachments found for the parent records in this batch'));
            }
        }
        for (let i = 0; i < records.length; i += contentVersionBatchSize) {
            const batch = records.slice(i, i + contentVersionBatchSize);
            // Request all ContentDocumentLink related to all records of the batch
            const linkedEntityIdIn = batch.map((record) => `'${record.Id}'`).join(',');
            const linkedEntityInQuery = `SELECT ContentDocumentId,LinkedEntityId FROM ContentDocumentLink WHERE LinkedEntityId IN (${linkedEntityIdIn})`;
            this.totalSoqlRequests++;
            const contentDocumentLinks = await bulkQueryByChunks(linkedEntityInQuery, this.conn, this.parentRecordsChunkSize);
            if (contentDocumentLinks.records.length > 0) {
                // Retrieve all ContentVersion related to ContentDocumentLink
                const contentDocIdIn = contentDocumentLinks.records.map((link) => `'${link.ContentDocumentId}'`);
                // Loop on contentDocIdIn by contentVersionBatchSize
                for (let j = 0; j < contentDocIdIn.length; j += contentVersionBatchSize) {
                    const contentDocIdBatch = contentDocIdIn.slice(j, j + contentVersionBatchSize).join(',');
                    // Log the progression of contentDocIdBatch
                    uxLog("action", this, c.cyan(`Processing ContentDocumentId chunk #${Math.ceil((j + 1) / contentVersionBatchSize)} on ${Math.ceil(contentDocIdIn.length / contentVersionBatchSize)}`));
                    // Request all ContentVersion related to all records of the batch
                    const contentVersionSoql = `SELECT Id,ContentDocumentId,Description,FileExtension,FileType,PathOnClient,Title FROM ContentVersion WHERE ContentDocumentId IN (${contentDocIdBatch}) AND IsLatest = true`;
                    this.totalSoqlRequests++;
                    const contentVersions = await bulkQueryByChunks(contentVersionSoql, this.conn, this.parentRecordsChunkSize);
                    // ContentDocument object can be linked to multiple other objects even with same type (for example: same attachment can be linked to multiple EmailMessage objects).
                    // Because of this when we fetch ContentVersion for ContentDocument it can return less results than there is ContentDocumentLink objects to link.
                    // To fix this we create a list of ContentVersion and ContentDocumentLink pairs.
                    // This way we have multiple pairs and we will download ContentVersion objects for each linked object.
                    const versionsAndLinks = [];
                    contentVersions.records.forEach((contentVersion) => {
                        contentDocumentLinks.records.forEach((contentDocumentLink) => {
                            if (contentDocumentLink.ContentDocumentId === contentVersion.ContentDocumentId) {
                                versionsAndLinks.push({
                                    contentVersion: contentVersion,
                                    contentDocumentLink: contentDocumentLink,
                                });
                            }
                        });
                    });
                    // Download files
                    await PromisePool.withConcurrency(5)
                        .for(versionsAndLinks)
                        .process(async (versionAndLink) => {
                        try {
                            await this.downloadContentVersionFile(versionAndLink.contentVersion, batch, versionAndLink.contentDocumentLink);
                        }
                        catch (e) {
                            this.filesErrors++;
                            uxLog("error", this, c.red('Download file error: ' + versionAndLink.contentVersion.Title + '\n' + e));
                        }
                    });
                }
            }
            else {
                uxLog("log", this, c.grey('No ContentDocumentLinks found for the parent records in this batch'));
            }
        }
    }
    async downloadFile(fetchUrl, outputFile) {
        const downloadResult = await new FileDownloader(fetchUrl, { conn: this.conn, outputFile: outputFile, label: 'file' }).download();
        if (downloadResult.success) {
            this.filesDownloaded++;
        }
        else {
            this.filesErrors++;
        }
    }
    async downloadAttachmentFile(attachment, records) {
        // Retrieve initial record to build output files folder name
        const parentAttachment = records.filter((record) => record.Id === attachment.ParentId)[0];
        // Build record output files folder (if folder name contains slashes or antislashes, replace them by spaces)
        const attachmentParentFolderName = (parentAttachment[this.dtl.outputFolderNameField] || parentAttachment.Id).replace(/[/\\?%*:|"<>]/g, '-');
        const parentRecordFolderForFiles = path.resolve(path.join(this.exportedFilesFolder, attachmentParentFolderName));
        // Define name of the file
        const outputFile = path.join(parentRecordFolderForFiles, attachment.Name.replace(/[/\\?%*:|"<>]/g, '-'));
        // Create directory if not existing
        await fs.ensureDir(parentRecordFolderForFiles);
        // Download file locally
        const fetchUrl = `${this.conn.instanceUrl}/services/data/v${getApiVersion()}/sobjects/Attachment/${attachment.Id}/Body`;
        await this.downloadFile(fetchUrl, outputFile);
    }
    async downloadContentVersionFile(contentVersion, records, contentDocumentLink) {
        // Retrieve initial record to build output files folder name
        const parentRecord = records.filter((record) => record.Id === contentDocumentLink.LinkedEntityId)[0];
        // Build record output files folder (if folder name contains slashes or antislashes, replace them by spaces)
        const parentFolderName = (parentRecord[this.dtl.outputFolderNameField] || parentRecord.Id).replace(/[/\\?%*:|"<>]/g, '-');
        const parentRecordFolderForFiles = path.resolve(path.join(this.exportedFilesFolder, parentFolderName));
        // Define name of the file
        let outputFile = 
        // Id
        this.dtl?.outputFileNameFormat === 'id'
            ? path.join(parentRecordFolderForFiles, contentVersion.Id)
            : // Title + Id
                this.dtl?.outputFileNameFormat === 'title_id'
                    ? path.join(parentRecordFolderForFiles, `${contentVersion.Title.replace(/[/\\?%*:|"<>]/g, '-')}_${contentVersion.Id}`)
                    : // Id + Title
                        this.dtl?.outputFileNameFormat === 'id_title'
                            ? path.join(parentRecordFolderForFiles, `${contentVersion.Id}_${contentVersion.Title.replace(/[/\\?%*:|"<>]/g, '-')}`)
                            : // Title
                                path.join(parentRecordFolderForFiles, contentVersion.Title.replace(/[/\\?%*:|"<>]/g, '-'));
        // Add file extension if missing in file title, and replace .snote by .html
        if (contentVersion.FileExtension && path.extname(outputFile) !== contentVersion.FileExtension) {
            outputFile =
                outputFile + '.' + (contentVersion.FileExtension !== 'snote' ? contentVersion.FileExtension : 'html');
        }
        // Check file extension
        if (this.dtl.fileTypes !== 'all' && !this.dtl.fileTypes.includes(contentVersion.FileType)) {
            uxLog("log", this, c.grey(`Skipped - ${outputFile.replace(this.exportedFilesFolder, '')} - File type ignored`));
            this.filesIgnoredType++;
            return;
        }
        // Check file overwrite
        if (this.dtl.overwriteFiles !== true && fs.existsSync(outputFile)) {
            uxLog("warning", this, c.yellow(`Skipped - ${outputFile.replace(this.exportedFilesFolder, '')} - File already existing`));
            this.filesIgnoredExisting++;
            return;
        }
        // Create directory if not existing
        await fs.ensureDir(parentRecordFolderForFiles);
        // Download file locally
        const fetchUrl = `${this.conn.instanceUrl}/services/data/v${getApiVersion()}/sobjects/ContentVersion/${contentVersion.Id}/VersionData`;
        await this.downloadFile(fetchUrl, outputFile);
    }
    // Build stats & result
    async buildResult() {
        const connAny = this.conn;
        const apiCallsRemaining = connAny?.limitInfo?.apiUsage?.used
            ? (connAny?.limitInfo?.apiUsage?.limit || 0) - (connAny?.limitInfo?.apiUsage?.used || 0)
            : null;
        uxLog("action", this, c.cyan(`API limit: ${c.bold(connAny?.limitInfo?.apiUsage?.limit || null)}`));
        uxLog("action", this, c.cyan(`API used before process: ${c.bold(this.apiUsedBefore)}`));
        uxLog("action", this, c.cyan(`API used after process: ${c.bold(connAny?.limitInfo?.apiUsage?.used || null)}`));
        uxLog("action", this, c.cyan(`API calls remaining for today: ${c.bold(apiCallsRemaining)}`));
        uxLog("action", this, c.cyan(`Total SOQL requests: ${c.bold(this.totalSoqlRequests)}`));
        uxLog("action", this, c.cyan(`Total parent records found: ${c.bold(this.totalParentRecords)}`));
        uxLog("action", this, c.cyan(`Total parent records with files: ${c.bold(this.parentRecordsWithFiles)}`));
        uxLog("action", this, c.cyan(`Total parent records ignored because already existing: ${c.bold(this.recordsIgnored)}`));
        uxLog("action", this, c.cyan(`Total files downloaded: ${c.bold(this.filesDownloaded)}`));
        uxLog("action", this, c.cyan(`Total file download errors: ${c.bold(this.filesErrors)}`));
        uxLog("action", this, c.cyan(`Total file skipped because of type constraint: ${c.bold(this.filesIgnoredType)}`));
        uxLog("action", this, c.cyan(`Total file skipped because previously downloaded: ${c.bold(this.filesIgnoredExisting)}`));
        return {
            totalParentRecords: this.totalParentRecords,
            parentRecordsWithFiles: this.parentRecordsWithFiles,
            filesDownloaded: this.filesDownloaded,
            filesErrors: this.filesErrors,
            recordsIgnored: this.recordsIgnored,
            filesIgnoredType: this.filesIgnoredType,
            filesIgnoredExisting: this.filesIgnoredExisting,
            apiLimit: connAny?.limitInfo?.apiUsage?.limit || null,
            apiUsedBefore: this.apiUsedBefore,
            apiUsedAfter: connAny?.limitInfo?.apiUsage?.used || null,
            apiCallsRemaining,
        };
    }
}
export class FilesImporter {
    filesPath;
    conn;
    commandThis;
    dtl = null; // export config
    exportedFilesFolder = '';
    handleOverwrite = false;
    constructor(filesPath, conn, options, commandThis) {
        this.filesPath = filesPath;
        this.exportedFilesFolder = path.join(this.filesPath, 'export');
        this.handleOverwrite = options?.handleOverwrite === true;
        this.conn = conn;
        this.commandThis = commandThis;
        if (options.exportConfig) {
            this.dtl = options.exportConfig;
        }
    }
    async processImport() {
        // Get config
        if (this.dtl === null) {
            this.dtl = await getFilesWorkspaceDetail(this.filesPath);
        }
        uxLog("action", this.commandThis, c.cyan(`Importing files from ${c.green(this.dtl.full_label)} ...`));
        uxLog("log", this.commandThis, c.italic(c.grey(this.dtl.description)));
        // Get folders and files
        const allRecordFolders = fs.readdirSync(this.exportedFilesFolder).filter((file) => {
            return fs.statSync(path.join(this.exportedFilesFolder, file)).isDirectory();
        });
        let totalFilesNumber = 0;
        for (const folder of allRecordFolders) {
            totalFilesNumber += fs.readdirSync(path.join(this.exportedFilesFolder, folder)).length;
        }
        await this.calculateApiConsumption(totalFilesNumber);
        // Query parent objects to find Ids corresponding to field value used as folder name
        const parentObjectsRes = await bulkQuery(this.dtl.soqlQuery, this.conn);
        const parentObjects = parentObjectsRes.records;
        let successNb = 0;
        let errorNb = 0;
        for (const recordFolder of allRecordFolders) {
            uxLog("log", this, c.grey(`Processing record ${recordFolder} ...`));
            const recordFolderPath = path.join(this.exportedFilesFolder, recordFolder);
            // List files in folder
            const files = fs.readdirSync(recordFolderPath).filter((file) => {
                return fs.statSync(path.join(this.exportedFilesFolder, recordFolder, file)).isFile();
            });
            // Find Id of parent object using folder name
            const parentRecordIds = parentObjects.filter((parentObj) => parentObj[this.dtl.outputFolderNameField] === recordFolder);
            if (parentRecordIds.length === 0) {
                uxLog("error", this, c.red(`Unable to find Id for ${this.dtl.outputFolderNameField}=${recordFolder}`));
                continue;
            }
            const parentRecordId = parentRecordIds[0].Id;
            let existingDocuments = [];
            // Collect existing documents if we handle file overwrite
            if (this.handleOverwrite) {
                const existingDocsQuery = `SELECT Id, ContentDocumentId, Title FROM ContentVersion WHERE FirstPublishLocationId = '${parentRecordId}'`;
                const existingDocsQueryRes = await this.conn.query(existingDocsQuery);
                existingDocuments = existingDocsQueryRes.records;
            }
            for (const file of files) {
                const fileData = fs.readFileSync(path.join(recordFolderPath, file));
                const contentVersionParams = {
                    Title: file,
                    PathOnClient: file,
                    VersionData: fileData.toString('base64'),
                };
                const matchingExistingDocs = existingDocuments.filter((doc) => doc.Title === file);
                if (matchingExistingDocs.length > 0) {
                    contentVersionParams.ContentDocumentId = matchingExistingDocs[0].ContentDocumentId;
                    uxLog("log", this, c.grey(`Overwriting file ${file} ...`));
                }
                else {
                    contentVersionParams.FirstPublishLocationId = parentRecordId;
                    uxLog("log", this, c.grey(`Uploading file ${file} ...`));
                }
                try {
                    const insertResult = await this.conn.sobject('ContentVersion').create(contentVersionParams);
                    if (insertResult.length === 0) {
                        uxLog("error", this, c.red(`Unable to upload file ${file}`));
                        errorNb++;
                    }
                    else {
                        successNb++;
                    }
                }
                catch (e) {
                    uxLog("error", this, c.red(`Unable to upload file ${file}: ${e.message}`));
                    errorNb++;
                }
            }
        }
        uxLog("success", this, c.green(`Uploaded ${successNb} files`));
        if (errorNb > 0) {
            uxLog("warning", this, c.yellow(`Errors during the upload of ${successNb} files`));
        }
        return { successNb: successNb, errorNb: errorNb };
    }
    // Calculate API consumption
    async calculateApiConsumption(totalFilesNumber) {
        const bulkCallsNb = 1;
        if (this.handleOverwrite) {
            totalFilesNumber = totalFilesNumber * 2;
        }
        // Check if there are enough API calls available
        // Request user confirmation
        if (!isCI) {
            const warningMessage = c.cyanBright(`Files import consumes one REST API call per uploaded file.
        (Estimation: ${bulkCallsNb} Bulks calls and ${totalFilesNumber} REST calls) Do you confirm you want to proceed ?`);
            const promptRes = await prompts({
                type: 'confirm',
                message: warningMessage,
                description: 'Confirm file import operation which will consume API calls'
            });
            if (promptRes.value !== true) {
                throw new SfError('Command cancelled by user');
            }
        }
    }
}
export async function selectFilesWorkspace(opts = { selectFilesLabel: 'Please select a files folder to export' }) {
    if (!fs.existsSync(filesFolderRoot)) {
        throw new SfError("There is no files root folder 'scripts/files' in your workspace. Create it and define a files export configuration");
    }
    const filesFolders = fs
        .readdirSync(filesFolderRoot, { withFileTypes: true })
        .filter((dirent) => dirent.isDirectory())
        .map((dirent) => path.join('.', 'scripts', 'files', dirent.name));
    if (filesFolders.length === 0) {
        throw new SfError('There is no file exports folder in your workspace');
    }
    const choices = [];
    for (const filesFolder of filesFolders) {
        const dtl = await getFilesWorkspaceDetail(filesFolder);
        if (dtl !== null) {
            choices.push({
                title: `📁 ${dtl.full_label}`,
                description: dtl.description,
                value: filesFolder,
            });
        }
    }
    const filesDirResult = await prompts({
        type: 'select',
        name: 'value',
        message: c.cyanBright(opts.selectFilesLabel),
        description: 'Select the files workspace configuration to use for this operation',
        choices: choices,
    });
    return filesDirResult.value;
}
export async function getFilesWorkspaceDetail(filesWorkspace) {
    const exportFile = path.join(filesWorkspace, 'export.json');
    if (!fs.existsSync(exportFile)) {
        uxLog("warning", this, c.yellow(`Your File export folder ${c.bold(filesWorkspace)} must contain an ${c.bold('export.json')} configuration file`));
        return null;
    }
    const exportFileJson = JSON.parse(await fs.readFile(exportFile, 'utf8'));
    const folderName = (filesWorkspace.replace(/\\/g, '/').match(/([^/]*)\/*$/) || '')[1];
    const hardisLabel = exportFileJson.sfdxHardisLabel || folderName;
    const hardisDescription = exportFileJson.sfdxHardisDescription || filesWorkspace;
    const soqlQuery = exportFileJson.soqlQuery || '';
    const fileTypes = exportFileJson.fileTypes || 'all';
    const outputFolderNameField = exportFileJson.outputFolderNameField || 'Name';
    const outputFileNameFormat = exportFileJson.outputFileNameFormat || 'title';
    const overwriteParentRecords = exportFileJson.overwriteParentRecords === false ? false : exportFileJson.overwriteParentRecords || true;
    const overwriteFiles = exportFileJson.overwriteFiles || false;
    return {
        full_label: `[${folderName}]${folderName != hardisLabel ? `: ${hardisLabel}` : ''}`,
        label: hardisLabel,
        description: hardisDescription,
        soqlQuery: soqlQuery,
        fileTypes: fileTypes,
        outputFolderNameField: outputFolderNameField,
        outputFileNameFormat: outputFileNameFormat,
        overwriteParentRecords: overwriteParentRecords,
        overwriteFiles: overwriteFiles,
    };
}
export async function promptFilesExportConfiguration(filesExportConfig, override = false) {
    const questions = [];
    if (override === false) {
        questions.push(...[
            {
                type: 'text',
                name: 'filesExportPath',
                message: c.cyanBright('Please input the files export config folder name (PascalCase format)'),
                description: 'The folder name that will be created to store the export configuration and downloaded files',
                placeholder: 'Ex: OpportunitiesPDF',
            },
            {
                type: 'text',
                name: 'sfdxHardisLabel',
                message: c.cyanBright('Please input a label for the files export configuration'),
                description: 'A human-readable label that will identify this export configuration',
                initial: filesExportConfig.sfdxHardisLabel,
            },
            {
                type: 'text',
                name: 'sfdxHardisDescription',
                message: c.cyanBright('Please input a description of the files export configuration'),
                description: 'A detailed description explaining what this export configuration does',
                initial: filesExportConfig.sfdxHardisDescription,
            },
        ]);
    }
    questions.push(...[
        {
            type: 'text',
            name: 'soqlQuery',
            message: 'Please input the main SOQL Query to fetch the parent records of files (ContentVersions)',
            description: 'SOQL query that retrieves the parent records to which files are attached',
            placeholder: 'Ex: SELECT Id,Name from Opportunity',
            initial: filesExportConfig.soqlQuery,
        },
        {
            type: 'text',
            name: 'outputFolderNameField',
            message: 'Please input the field to use to build the name of the folder containing downloaded files',
            description: 'Field name from the SOQL query result that will be used as folder name for organizing files',
            placeholder: 'Ex: Name',
            initial: filesExportConfig.outputFolderNameField,
        },
        {
            type: 'select',
            name: 'outputFileNameFormat',
            choices: [
                { value: 'title', title: 'title (ex: "Cloudity New Project")' },
                { value: 'title_id', title: 'title_id (ex: "Cloudity New Project_006bR00000Bet7WQAR")' },
                { value: 'id_title', title: 'id_title (ex: "006bR00000Bet7WQAR_Cloudity New Project")' },
                { value: 'id', title: 'id (ex: "006bR00000Bet7WQAR")' },
            ],
            message: 'Please select the format of output files names',
            description: 'Choose how downloaded file names should be formatted',
            initial: filesExportConfig.outputFileNameFormat,
        },
        {
            type: 'confirm',
            name: 'overwriteParentRecords',
            message: 'Do you want to try to download files attached to a parent records whose folder is already existing in local folders ?',
            description: 'Allow downloading files for records that already have a local folder',
            initial: filesExportConfig.overwriteParentRecords,
        },
        {
            type: 'confirm',
            name: 'overwriteFiles',
            message: 'Do you want to overwrite file that has already been previously downloaded ?',
            description: 'Replace existing local files with newly downloaded versions',
            initial: filesExportConfig.overwriteFiles,
        },
    ]);
    const resp = await prompts(questions);
    const filesConfig = Object.assign(filesExportConfig, {
        filesExportPath: resp.filesExportPath,
        sfdxHardisLabel: resp.sfdxHardisLabel || filesExportConfig.sfdxHardisLabel,
        sfdxHardisDescription: resp.sfdxHardisDescription || filesExportConfig.sfdxHardisDescription,
        soqlQuery: resp.soqlQuery,
        outputFolderNameField: resp.outputFolderNameField,
        outputFileNameFormat: resp.outputFileNameFormat,
        overwriteParentRecords: resp.overwriteParentRecords,
        overwriteFiles: resp.overwriteFiles,
    });
    return filesConfig;
}
export async function countLinesInFile(file) {
    let readError;
    let lineCount = 0;
    return await new Promise((resolve) => {
        fs.createReadStream(file)
            .pipe(split())
            .on('data', () => {
            lineCount++;
        })
            .on('end', () => {
            if (readError) {
                return;
            }
            resolve(lineCount - 1);
        })
            .on('error', (error) => {
            readError = true;
            resolve(error);
        });
    });
}
/**
 * @description This function generates a report path for a given file name prefix.
 * It retrieves the report directory and the current branch name.
 * If the branch name is not available in the environment variable CI_COMMIT_REF_NAME, it tries to get the current git branch.
 * If both are not available, it uses the string "Missing CI_COMMIT_REF_NAME variable".
 * It then joins the report directory, file name prefix, and branch name to form the full path of the report.
 *
 * @param {string} fileNamePrefix - The prefix for the file name.
 * @param {string} outputFile - The output file path. If null, a new path is generated.
 * @param {Object} [options] - Additional options for generating the report path.
 * @param {boolean} [options.withDate=false] - Whether to append a timestamp to the file name.
 * @returns {Promise<string>} - A Promise that resolves to the full path of the report.
 */
export async function generateReportPath(fileNamePrefix, outputFile, options = { withDate: false }) {
    if (outputFile == null) {
        const reportDir = await getReportDirectory();
        const branchName = (!isGitRepo()) ? 'no-git' :
            process.env.CI_COMMIT_REF_NAME ||
                (await getCurrentGitBranch({ formatted: true })) ||
                'branch-not-found';
        let newOutputFile = path.join(reportDir, `${fileNamePrefix}-${branchName.split('/').pop()}.csv`);
        if (options.withDate) {
            // Add date time info
            const date = new Date().toISOString().replace(/[:.]/g, '-').replace('T', '_').split('.')[0];
            newOutputFile = path.join(reportDir, `${fileNamePrefix}-${branchName.split('/').pop()}-${date}.csv`);
        }
        return newOutputFile;
    }
    else {
        await fs.ensureDir(path.dirname(outputFile));
        return outputFile;
    }
}
/**
 * @description This function generates a CSV file from the provided data and writes it to the specified output path.
 * If the operation is successful, it logs a message and requests to open the file.
 * If an error occurs during the operation, it logs the error message and stack trace.
 *
 * @param {any[]} data - The data to be written to the CSV file.
 * @param {string} outputPath - The path where the CSV file will be written.
 * @returns {Promise<void>} - A Promise that resolves when the operation is complete.
 */
export async function generateCsvFile(data, outputPath, options) {
    const result = {};
    try {
        const csvContent = Papa.unparse(data);
        await fs.writeFile(outputPath, csvContent, 'utf8');
        uxLog("action", this, c.cyan(c.italic(`Please see detailed CSV log in ${c.bold(outputPath)}`)));
        result.csvFile = outputPath;
        if (!WebSocketClient.isAliveWithLwcUI()) {
            WebSocketClient.requestOpenFile(outputPath);
        }
        const csvFileTitle = options?.fileTitle ? `${options.fileTitle} (CSV)` : options?.csvFileTitle ?? "Report (CSV)";
        WebSocketClient.sendReportFileMessage(outputPath, csvFileTitle, "report");
        if (data.length > 0 && !options?.noExcel) {
            try {
                // Generate mirror XSLX file
                const xlsDirName = path.join(path.dirname(outputPath), 'xls');
                const xslFileName = path.basename(outputPath).replace('.csv', '.xlsx');
                const xslxFile = path.join(xlsDirName, xslFileName);
                await fs.ensureDir(xlsDirName);
                await csvToXls(outputPath, xslxFile);
                uxLog("action", this, c.cyan(c.italic(`Please see detailed XSLX log in ${c.bold(xslxFile)}`)));
                const xlsFileTitle = options?.fileTitle ? `${options.fileTitle} (XSLX)` : options?.xlsFileTitle ?? "Report (XSLX)";
                WebSocketClient.sendReportFileMessage(xslxFile, xlsFileTitle, "report");
                result.xlsxFile = xslxFile;
                if (!isCI && !(process.env.NO_OPEN === 'true') && !WebSocketClient.isAliveWithLwcUI()) {
                    try {
                        uxLog("other", this, c.italic(c.grey(`Opening XSLX file ${c.bold(xslxFile)}... (define NO_OPEN=true to disable this)`)));
                        await open(xslxFile, { wait: false });
                    }
                    catch (e) {
                        uxLog("warning", this, c.yellow('Error while opening XSLX file:\n' + e.message + '\n' + e.stack));
                    }
                }
            }
            catch (e2) {
                uxLog("warning", this, c.yellow('Error while generating XSLX log file:\n' + e2.message + '\n' + e2.stack));
            }
        }
        else {
            uxLog("other", this, c.grey(`No XLS file generated as ${outputPath} is empty`));
        }
    }
    catch (e) {
        uxLog("warning", this, c.yellow('Error while generating CSV log file:\n' + e.message + '\n' + e.stack));
    }
    return result;
}
async function csvToXls(csvFile, xslxFile) {
    const workbook = new ExcelJS.Workbook();
    const worksheet = await workbook.csv.readFile(csvFile);
    // Set filters
    worksheet.autoFilter = 'A1:Z1';
    // Adjust column size (only if the file is not too big, to avoid performances issues)
    if (worksheet.rowCount < 5000) {
        worksheet.columns.forEach((column) => {
            const lengths = (column.values || []).map((v) => (v || '').toString().length);
            const maxLength = Math.max(...lengths.filter((v) => typeof v === 'number'));
            column.width = maxLength;
        });
    }
    await workbook.xlsx.writeFile(xslxFile);
}
//# sourceMappingURL=filesUtils.js.map