/*
 * Copyright (c) 2025, Salesforce, Inc.
 * SPDX-License-Identifier: Apache-2
 * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0
 */
/**
 * Site archive import/export operations for B2C Commerce.
 *
 * Provides functions for importing and exporting site archives using
 * the sfcc-site-archive-import and sfcc-site-archive-export system jobs.
 */
import * as fs from 'node:fs';
import * as path from 'node:path';
import JSZip from 'jszip';
import { B2CInstance } from '../../instance/index.js';
import { getLogger } from '../../logging/logger.js';
import { waitForJob, JobExecutionError, getJobLog } from './run.js';
const IMPORT_JOB_ID = 'sfcc-site-archive-import';
const EXPORT_JOB_ID = 'sfcc-site-archive-export';
/**
 * Imports a site archive to a B2C Commerce instance.
 *
 * Supports importing from:
 * - A local directory (will be zipped automatically)
 * - A local zip file
 * - A Buffer containing zip data
 * - A filename already on the instance (in Impex/src/instance/)
 *
 * **Buffer handling:** When passing a Buffer, the `archiveName` option controls
 * the contract:
 * - **Without `archiveName`:** The buffer should contain archive entries without
 *   a root directory (e.g. `libraries/mylib/library.xml`). The SDK generates
 *   an archive name and wraps the contents under it.
 * - **With `archiveName`:** The buffer must already be correctly structured with
 *   `archiveName/` as the top-level directory. It is uploaded as-is.
 *
 * @param instance - B2C instance to import to
 * @param target - Source to import (directory path, zip file path, Buffer, or remote filename)
 * @param options - Import options
 * @returns Import result with execution details
 * @throws JobExecutionError if import job fails
 *
 * @example
 * ```typescript
 * // Import from a local directory
 * const result = await siteArchiveImport(instance, './my-site-data');
 *
 * // Import from a zip file
 * const result = await siteArchiveImport(instance, './export.zip');
 *
 * // Import from a buffer (SDK wraps contents automatically)
 * const zip = new JSZip();
 * zip.file('libraries/mylib/library.xml', xmlContent);
 * const buffer = await zip.generateAsync({type: 'nodebuffer'});
 * const result = await siteArchiveImport(instance, buffer);
 *
 * // Import from a buffer with explicit archive name (caller owns structure)
 * const zip = new JSZip();
 * zip.file('my-import/libraries/mylib/library.xml', xmlContent);
 * const buffer = await zip.generateAsync({type: 'nodebuffer'});
 * const result = await siteArchiveImport(instance, buffer, {
 *   archiveName: 'my-import'
 * });
 *
 * // Import from existing file on instance
 * const result = await siteArchiveImport(instance, {
 *   remoteFilename: 'existing-archive.zip'
 * });
 * ```
 */
export async function siteArchiveImport(instance, target, options = {}) {
    const logger = getLogger();
    const { keepArchive = false, waitOptions, archiveName } = options;
    let zipFilename;
    let needsUpload = true;
    let archiveContent;
    // Handle different input types
    if (typeof target === 'object' && 'remoteFilename' in target) {
        // Remote filename - no upload needed
        zipFilename = target.remoteFilename;
        needsUpload = false;
    }
    else if (Buffer.isBuffer(target)) {
        if (archiveName) {
            // Caller provides name — buffer must already contain the correct
            // top-level directory structure (archiveName/...).
            const baseName = archiveName.endsWith('.zip') ? archiveName.slice(0, -4) : archiveName;
            zipFilename = `${baseName}.zip`;
            archiveContent = target;
        }
        else {
            // No name — SDK generates one and wraps the buffer contents under it.
            // The buffer should contain archive entries without a root directory
            // (e.g. libraries/mylib/library.xml, sites/RefArch/site.xml).
            const archiveDirName = `import-${Date.now()}`;
            zipFilename = `${archiveDirName}.zip`;
            archiveContent = await wrapArchiveContents(target, archiveDirName, logger);
        }
    }
    else {
        // File path - check if directory or zip file
        const targetPath = target;
        if (!fs.existsSync(targetPath)) {
            throw new Error(`Target not found: ${targetPath}`);
        }
        const stat = await fs.promises.stat(targetPath);
        if (stat.isFile()) {
            // Existing zip file
            archiveContent = await fs.promises.readFile(targetPath);
            zipFilename = path.basename(targetPath);
        }
        else if (stat.isDirectory()) {
            // Directory - create zip archive
            const timestamp = Date.now();
            const archiveDirName = archiveName || `import-${timestamp}`;
            zipFilename = `${archiveDirName}.zip`;
            logger.debug({ path: targetPath }, `Creating archive from directory: ${targetPath}`);
            archiveContent = await createArchiveFromDirectory(targetPath, archiveDirName);
        }
        else {
            throw new Error(`Target must be a file or directory: ${targetPath}`);
        }
    }
    // Upload archive if needed
    const uploadPath = `Impex/src/instance/${zipFilename}`;
    if (needsUpload && archiveContent) {
        logger.debug({ path: uploadPath }, `Uploading archive to ${uploadPath}`);
        await instance.webdav.put(uploadPath, archiveContent, 'application/zip');
        logger.debug({ path: uploadPath }, `Archive uploaded: ${uploadPath}`);
    }
    // Execute the import job with file_name parameter
    logger.debug({ jobId: IMPORT_JOB_ID, file: zipFilename }, `Executing ${IMPORT_JOB_ID} job with file_name: ${zipFilename}`);
    let execution;
    // Try file_name format first (standard OCAPI format)
    const { data, error } = await instance.ocapi.POST('/jobs/{job_id}/executions', {
        params: { path: { job_id: IMPORT_JOB_ID } },
        body: { file_name: zipFilename },
    });
    if (error || !data) {
        // Try with parameters format as fallback
        logger.warn('Retrying with parameters format for internal users');
        const { data: retryData, error: retryError } = await instance.ocapi.POST('/jobs/{job_id}/executions', {
            params: { path: { job_id: IMPORT_JOB_ID } },
            body: {
                parameters: [{ name: 'ImportFile', value: zipFilename }],
            },
        });
        if (retryError || !retryData) {
            throw new Error(retryError?.fault?.message ?? error?.fault?.message ?? 'Failed to execute import job');
        }
        execution = retryData;
    }
    else {
        execution = data;
    }
    logger.debug({ jobId: IMPORT_JOB_ID, executionId: execution.id }, `Import job started: ${execution.id}`);
    // Wait for completion
    try {
        execution = await waitForJob(instance, IMPORT_JOB_ID, execution.id, waitOptions);
    }
    catch (error) {
        if (error instanceof JobExecutionError) {
            // Try to get log file
            try {
                const log = await getJobLog(instance, error.execution);
                logger.error({ jobId: IMPORT_JOB_ID, logFile: error.execution.log_file_path, log }, `Job log:\n${log}`);
            }
            catch {
                logger.error({ jobId: IMPORT_JOB_ID }, 'Could not retrieve job log');
            }
        }
        throw error;
    }
    // Clean up archive if not keeping
    if (!keepArchive && needsUpload) {
        await instance.webdav.delete(uploadPath);
        logger.debug({ path: uploadPath }, `Archive deleted: ${uploadPath}`);
    }
    return {
        execution,
        archiveFilename: zipFilename,
        archiveKept: keepArchive,
    };
}
/**
 * Creates a zip archive from a directory.
 */
async function createArchiveFromDirectory(dirPath, archiveDirName) {
    const zip = new JSZip();
    const rootFolder = zip.folder(archiveDirName);
    await addDirectoryToZip(rootFolder, dirPath);
    return zip.generateAsync({
        type: 'nodebuffer',
        compression: 'DEFLATE',
        compressionOptions: { level: 9 },
    });
}
/**
 * Recursively adds directory contents to a JSZip folder.
 */
async function addDirectoryToZip(zipFolder, dirPath) {
    const entries = await fs.promises.readdir(dirPath, { withFileTypes: true });
    for (const entry of entries) {
        const fullPath = path.join(dirPath, entry.name);
        if (entry.isDirectory()) {
            const subFolder = zipFolder.folder(entry.name);
            await addDirectoryToZip(subFolder, fullPath);
        }
        else if (entry.isFile()) {
            const content = await fs.promises.readFile(fullPath);
            zipFolder.file(entry.name, content);
        }
    }
}
/**
 * Wraps the contents of a zip buffer under a new top-level directory.
 *
 * The input buffer should contain archive entries without a root directory
 * (e.g. `libraries/mylib/library.xml`). The output will have all entries
 * nested under `archiveDirName/` (e.g. `archiveDirName/libraries/mylib/library.xml`).
 */
async function wrapArchiveContents(buffer, archiveDirName, logger) {
    const zip = await JSZip.loadAsync(buffer);
    logger.debug({ archiveDirName }, `Wrapping archive contents under ${archiveDirName}/`);
    const newZip = new JSZip();
    const rootFolder = newZip.folder(archiveDirName);
    for (const [filePath, entry] of Object.entries(zip.files)) {
        if (!entry.dir) {
            const content = await entry.async('nodebuffer');
            rootFolder.file(filePath, content);
        }
    }
    return newZip.generateAsync({
        type: 'nodebuffer',
        compression: 'DEFLATE',
        compressionOptions: { level: 9 },
    });
}
/**
 * Exports a site archive from a B2C Commerce instance.
 *
 * @param instance - B2C instance to export from
 * @param dataUnits - Data units configuration specifying what to export
 * @param options - Export options
 * @returns Export result with archive data
 * @throws JobExecutionError if export job fails
 *
 * @example
 * ```typescript
 * // Export global meta data
 * const result = await siteArchiveExport(instance, {
 *   global_data: { meta_data: true }
 * });
 *
 * // Export a site's content
 * const result = await siteArchiveExport(instance, {
 *   sites: {
 *     'RefArch': { content: true, site_preferences: true }
 *   }
 * });
 *
 * // Export catalogs
 * const result = await siteArchiveExport(instance, {
 *   catalogs: { 'storefront-catalog': true }
 * });
 * ```
 */
export async function siteArchiveExport(instance, dataUnits, options = {}) {
    const logger = getLogger();
    const { waitOptions } = options;
    // Generate archive filename
    const timestamp = new Date().toISOString().replace(/[:.-]+/g, '');
    const archiveDirName = `${timestamp}_export`;
    const zipFilename = `${archiveDirName}.zip`;
    logger.debug({ jobId: EXPORT_JOB_ID, dataUnits }, `Executing ${EXPORT_JOB_ID} job`);
    let execution;
    // Execute export job - try export_file format first
    try {
        const { data, error } = await instance.ocapi.POST('/jobs/{job_id}/executions', {
            params: { path: { job_id: EXPORT_JOB_ID } },
            body: {
                export_file: zipFilename,
                data_units: dataUnits,
            },
        });
        if (error || !data) {
            throw new Error(error?.fault?.message ?? 'Failed to execute export job');
        }
        execution = data;
    }
    catch {
        // Try parameters format for internal users
        logger.warn('Retrying with parameters format for internal users');
        const { data, error } = await instance.ocapi.POST('/jobs/{job_id}/executions', {
            params: { path: { job_id: EXPORT_JOB_ID } },
            body: {
                parameters: [
                    { name: 'ExportFile', value: zipFilename },
                    { name: 'DataUnits', value: JSON.stringify(dataUnits) },
                ],
            },
        });
        if (error || !data) {
            throw new Error(error?.fault?.message ?? 'Failed to execute export job');
        }
        execution = data;
    }
    logger.debug({ jobId: EXPORT_JOB_ID, executionId: execution.id }, `Export job started: ${execution.id}`);
    // Wait for completion
    try {
        execution = await waitForJob(instance, EXPORT_JOB_ID, execution.id, waitOptions);
    }
    catch (error) {
        if (error instanceof JobExecutionError) {
            // Try to get log file
            try {
                const log = await getJobLog(instance, error.execution);
                logger.error({ jobId: EXPORT_JOB_ID, logFile: error.execution.log_file_path, log }, `Job log:\n${log}`);
            }
            catch {
                logger.error({ jobId: EXPORT_JOB_ID }, 'Could not retrieve job log');
            }
        }
        throw error;
    }
    return {
        execution,
        archiveFilename: zipFilename,
    };
}
/**
 * Exports a site archive and downloads it to memory.
 *
 * Runs the export job on the instance, downloads the archive via WebDAV,
 * and returns the data as a Buffer. Optionally keeps the archive on the instance.
 *
 * @param instance - B2C instance to export from
 * @param dataUnits - Data units configuration specifying what to export
 * @param options - Export and download options
 * @returns Export result with archive data buffer
 *
 * @example
 * ```typescript
 * const result = await siteArchiveExportDownload(instance, {
 *   global_data: { meta_data: true }
 * });
 * const zip = await JSZip.loadAsync(result.data);
 * ```
 */
export async function siteArchiveExportToBuffer(instance, dataUnits, options = {}) {
    const logger = getLogger();
    const { keepArchive = false, ...exportOptions } = options;
    const result = await siteArchiveExport(instance, dataUnits, exportOptions);
    // Download archive from instance via WebDAV
    const webdavPath = `Impex/src/instance/${result.archiveFilename}`;
    logger.debug({ path: webdavPath }, `Downloading archive: ${webdavPath}`);
    const data = Buffer.from(await instance.webdav.get(webdavPath));
    // Clean up from instance if not keeping
    if (!keepArchive) {
        await instance.webdav.delete(webdavPath);
        logger.debug({ path: webdavPath }, `Archive deleted: ${webdavPath}`);
    }
    return {
        ...result,
        data,
        archiveKept: keepArchive,
    };
}
/**
 * Exports a site archive, downloads it, and saves it to a local path.
 *
 * Runs the export job on the instance, downloads the archive via WebDAV,
 * and saves it locally. Optionally keeps the archive on the instance.
 *
 * @param instance - B2C instance to export from
 * @param dataUnits - Data units configuration
 * @param outputPath - Local path to save the archive
 * @param options - Export and download options
 * @returns Export result with local path
 *
 * @example
 * ```typescript
 * // Export and save to a directory (extracts zip)
 * await siteArchiveExportToPath(instance, { global_data: { meta_data: true } }, './exports');
 *
 * // Export and save as zip
 * await siteArchiveExportToPath(instance, { global_data: { meta_data: true } }, './exports/archive.zip');
 * ```
 */
export async function siteArchiveExportToPath(instance, dataUnits, outputPath, options = {}) {
    const logger = getLogger();
    const { extractZip = true, ...downloadOptions } = options;
    const result = await siteArchiveExportToBuffer(instance, dataUnits, downloadOptions);
    // Determine output handling
    const isZipPath = outputPath.endsWith('.zip');
    if (isZipPath || !extractZip) {
        // Save as zip file
        const zipPath = isZipPath ? outputPath : path.join(outputPath, result.archiveFilename);
        // Ensure directory exists
        await fs.promises.mkdir(path.dirname(zipPath), { recursive: true });
        await fs.promises.writeFile(zipPath, result.data);
        logger.debug({ path: zipPath }, `Archive saved to: ${zipPath}`);
        return {
            ...result,
            localPath: zipPath,
        };
    }
    else {
        // Extract to directory
        await fs.promises.mkdir(outputPath, { recursive: true });
        const zip = await JSZip.loadAsync(result.data);
        for (const [relativePath, zipEntry] of Object.entries(zip.files)) {
            const fullPath = path.join(outputPath, relativePath);
            if (zipEntry.dir) {
                await fs.promises.mkdir(fullPath, { recursive: true });
            }
            else {
                // Ensure parent directory exists
                await fs.promises.mkdir(path.dirname(fullPath), { recursive: true });
                const content = await zipEntry.async('nodebuffer');
                await fs.promises.writeFile(fullPath, content);
            }
        }
        logger.debug({ path: outputPath }, `Archive extracted to: ${outputPath}`);
        return {
            ...result,
            localPath: outputPath,
        };
    }
}
//# sourceMappingURL=site-archive.js.map